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() {
|
||||
MissionsFragment fragment = new MissionsFragment();
|
||||
|
||||
getFragmentManager().beginTransaction()
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.frame, fragment, MISSIONS_FRAGMENT_TAG)
|
||||
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||
.commit();
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
package org.schabi.newpipe.download;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.IdRes;
|
||||
import android.support.annotation.NonNull;
|
||||
|
@ -14,6 +20,7 @@ import android.support.v7.widget.Toolbar;
|
|||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
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.VideoStream;
|
||||
import org.schabi.newpipe.extractor.utils.Localization;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.FilenameUtils;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
|
@ -44,20 +52,27 @@ import org.schabi.newpipe.util.StreamItemAdapter;
|
|||
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import us.shandian.giga.io.StoredDirectoryHelper;
|
||||
import us.shandian.giga.io.StoredFileHelper;
|
||||
import us.shandian.giga.postprocessing.Postprocessing;
|
||||
import us.shandian.giga.service.DownloadManager;
|
||||
import us.shandian.giga.service.DownloadManagerService;
|
||||
import us.shandian.giga.service.DownloadManagerService.MissionCheck;
|
||||
import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder;
|
||||
import us.shandian.giga.service.MissionState;
|
||||
|
||||
public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
|
||||
private static final String TAG = "DialogFragment";
|
||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||
private static final int REQUEST_DOWNLOAD_PATH_SAF = 0x1230;
|
||||
|
||||
@State
|
||||
protected StreamInfo currentInfo;
|
||||
|
@ -82,7 +97,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
|
||||
private EditText nameEditText;
|
||||
private Spinner streamsSpinner;
|
||||
private RadioGroup radioVideoAudioGroup;
|
||||
private RadioGroup radioStreamsGroup;
|
||||
private TextView threadsCountTextView;
|
||||
private SeekBar threadsSeekBar;
|
||||
|
||||
|
@ -162,7 +177,11 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
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);
|
||||
|
||||
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.audioStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedAudioStreams);
|
||||
this.subtitleStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedSubtitleStreams);
|
||||
this.videoStreamsAdapter = new StreamItemAdapter<>(context, wrappedVideoStreams, secondaryStreams);
|
||||
this.audioStreamsAdapter = new StreamItemAdapter<>(context, wrappedAudioStreams);
|
||||
this.subtitleStreamsAdapter = new StreamItemAdapter<>(context, wrappedSubtitleStreams);
|
||||
|
||||
Intent intent = new Intent(context, DownloadManagerService.class);
|
||||
context.startService(intent);
|
||||
|
||||
context.bindService(intent, new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName cname, IBinder service) {
|
||||
DownloadManagerBinder mgr = (DownloadManagerBinder) service;
|
||||
|
||||
mainStorageAudio = mgr.getMainStorageAudio();
|
||||
mainStorageVideo = mgr.getMainStorageVideo();
|
||||
downloadManager = mgr.getDownloadManager();
|
||||
|
||||
okButton.setEnabled(true);
|
||||
|
||||
context.unbindService(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
// nothing to do
|
||||
}
|
||||
}, Context.BIND_AUTO_CREATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -206,8 +248,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
threadsCountTextView = view.findViewById(R.id.threads_count);
|
||||
threadsSeekBar = view.findViewById(R.id.threads);
|
||||
|
||||
radioVideoAudioGroup = view.findViewById(R.id.video_audio_group);
|
||||
radioVideoAudioGroup.setOnCheckedChangeListener(this);
|
||||
radioStreamsGroup = view.findViewById(R.id.video_audio_group);
|
||||
radioStreamsGroup.setOnCheckedChangeListener(this);
|
||||
|
||||
initToolbar(view.findViewById(R.id.toolbar));
|
||||
setupDownloadOptions();
|
||||
|
@ -242,17 +284,17 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
disposables.clear();
|
||||
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams).subscribe(result -> {
|
||||
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.video_button) {
|
||||
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.video_button) {
|
||||
setupVideoSpinner();
|
||||
}
|
||||
}));
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams).subscribe(result -> {
|
||||
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
|
||||
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.audio_button) {
|
||||
setupAudioSpinner();
|
||||
}
|
||||
}));
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams).subscribe(result -> {
|
||||
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
|
||||
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
|
||||
setupSubtitleSpinner();
|
||||
}
|
||||
}));
|
||||
|
@ -270,17 +312,40 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
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
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void initToolbar(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.setNavigationIcon(ThemeHelper.isLightThemeSelected(getActivity()) ? R.drawable.ic_arrow_back_black_24dp : R.drawable.ic_arrow_back_white_24dp);
|
||||
toolbar.setNavigationIcon(isLight ? R.drawable.ic_arrow_back_black_24dp : R.drawable.ic_arrow_back_white_24dp);
|
||||
toolbar.inflateMenu(R.menu.dialog_url);
|
||||
toolbar.setNavigationOnClickListener(v -> getDialog().dismiss());
|
||||
|
||||
|
||||
toolbar.setOnMenuItemClickListener(item -> {
|
||||
if (item.getItemId() == R.id.okay) {
|
||||
prepareSelectedDownload();
|
||||
|
@ -348,7 +413,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "onItemSelected() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]");
|
||||
switch (radioVideoAudioGroup.getCheckedRadioButtonId()) {
|
||||
switch (radioStreamsGroup.getCheckedRadioButtonId()) {
|
||||
case R.id.audio_button:
|
||||
selectedAudioIndex = position;
|
||||
break;
|
||||
|
@ -372,9 +437,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
protected void setupDownloadOptions() {
|
||||
setRadioButtonsState(false);
|
||||
|
||||
final RadioButton audioButton = radioVideoAudioGroup.findViewById(R.id.audio_button);
|
||||
final RadioButton videoButton = radioVideoAudioGroup.findViewById(R.id.video_button);
|
||||
final RadioButton subtitleButton = radioVideoAudioGroup.findViewById(R.id.subtitle_button);
|
||||
final RadioButton audioButton = radioStreamsGroup.findViewById(R.id.audio_button);
|
||||
final RadioButton videoButton = radioStreamsGroup.findViewById(R.id.video_button);
|
||||
final RadioButton subtitleButton = radioStreamsGroup.findViewById(R.id.subtitle_button);
|
||||
final boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0;
|
||||
final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0;
|
||||
final boolean isSubtitleStreamsAvailable = subtitleStreamsAdapter.getCount() > 0;
|
||||
|
@ -399,9 +464,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
}
|
||||
|
||||
private void setRadioButtonsState(boolean enabled) {
|
||||
radioVideoAudioGroup.findViewById(R.id.audio_button).setEnabled(enabled);
|
||||
radioVideoAudioGroup.findViewById(R.id.video_button).setEnabled(enabled);
|
||||
radioVideoAudioGroup.findViewById(R.id.subtitle_button).setEnabled(enabled);
|
||||
radioStreamsGroup.findViewById(R.id.audio_button).setEnabled(enabled);
|
||||
radioStreamsGroup.findViewById(R.id.video_button).setEnabled(enabled);
|
||||
radioStreamsGroup.findViewById(R.id.subtitle_button).setEnabled(enabled);
|
||||
}
|
||||
|
||||
private int getSubtitleIndexBy(List<SubtitlesStream> streams) {
|
||||
|
@ -436,56 +501,113 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
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() {
|
||||
final Context context = getContext();
|
||||
Stream stream;
|
||||
String location;
|
||||
char kind;
|
||||
StoredDirectoryHelper mainStorage;
|
||||
MediaFormat format;
|
||||
String mime;
|
||||
|
||||
String fileName = nameEditText.getText().toString().trim();
|
||||
if (fileName.isEmpty())
|
||||
fileName = FilenameUtils.createFilename(context, currentInfo.getName());
|
||||
// first, build the filename and get the output folder (if possible)
|
||||
|
||||
switch (radioVideoAudioGroup.getCheckedRadioButtonId()) {
|
||||
String filename = getNameEditText() + ".";
|
||||
if (filename.isEmpty()) {
|
||||
filename = FilenameUtils.createFilename(context, currentInfo.getName());
|
||||
}
|
||||
filename += ".";
|
||||
|
||||
switch (radioStreamsGroup.getCheckedRadioButtonId()) {
|
||||
case R.id.audio_button:
|
||||
stream = audioStreamsAdapter.getItem(selectedAudioIndex);
|
||||
location = NewPipeSettings.getAudioDownloadPath(context);
|
||||
kind = 'a';
|
||||
mainStorage = mainStorageAudio;
|
||||
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
|
||||
mime = format.mimeType;
|
||||
filename += format.suffix;
|
||||
break;
|
||||
case R.id.video_button:
|
||||
stream = videoStreamsAdapter.getItem(selectedVideoIndex);
|
||||
location = NewPipeSettings.getVideoDownloadPath(context);
|
||||
kind = 'v';
|
||||
mainStorage = mainStorageVideo;
|
||||
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
|
||||
mime = format.mimeType;
|
||||
filename += format.suffix;
|
||||
break;
|
||||
case R.id.subtitle_button:
|
||||
stream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
|
||||
location = NewPipeSettings.getVideoDownloadPath(context);// assume that subtitle & video files go together
|
||||
kind = 's';
|
||||
mainStorage = mainStorageVideo;// subtitle & video files go together
|
||||
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
|
||||
mime = format.mimeType;
|
||||
filename += format == MediaFormat.TTML ? MediaFormat.SRT.suffix : format.suffix;
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("No stream selected");
|
||||
}
|
||||
|
||||
if (mainStorage == null) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
int threads;
|
||||
// check for existing file with the same name
|
||||
Uri result = mainStorage.findFile(filename);
|
||||
|
||||
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
|
||||
threads = 1;// use unique thread for subtitles due small file size
|
||||
fileName += ".srt";// final subtitle format
|
||||
} else {
|
||||
threads = threadsSeekBar.getProgress() + 1;
|
||||
fileName += "." + stream.getFormat().getSuffix();
|
||||
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;
|
||||
}
|
||||
|
||||
final String finalFileName = fileName;
|
||||
continueSelectedDownload(storage);
|
||||
return;
|
||||
}
|
||||
|
||||
DownloadManagerService.checkForRunningMission(context, location, fileName, (MissionCheck result) -> {
|
||||
// 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 (result) {
|
||||
switch (state) {
|
||||
case Finished:
|
||||
msgBtn = R.string.overwrite;
|
||||
msgBody = R.string.overwrite_warning;
|
||||
msgBody = R.string.overwrite_finished_warning;
|
||||
break;
|
||||
case Pending:
|
||||
msgBtn = R.string.overwrite;
|
||||
|
@ -495,45 +617,108 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
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;
|
||||
default:
|
||||
downloadSelected(context, stream, location, finalFileName, kind, threads);
|
||||
return;
|
||||
}
|
||||
|
||||
// overwrite or unique name actions are done by the download manager
|
||||
|
||||
// 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) -> downloadSelected(context, stream, location, finalFileName, kind, threads)
|
||||
)
|
||||
.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel())
|
||||
.setPositiveButton(msgBtn, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
|
||||
StoredFileHelper storageNew;
|
||||
switch (state) {
|
||||
case Finished:
|
||||
case Pending:
|
||||
downloadManager.forgetMission(storage);
|
||||
case None:
|
||||
// try take (or steal) the file permissions
|
||||
try {
|
||||
storageNew = new StoredFileHelper(context, result, mainStorage.getTag());
|
||||
if (storageNew.canWrite())
|
||||
continueSelectedDownload(storageNew);
|
||||
else
|
||||
showFailedDialog(R.string.error_file_creation);
|
||||
} catch (IOException e) {
|
||||
showErrorActivity(e);
|
||||
}
|
||||
break;
|
||||
case PendingRunning:
|
||||
// FIXME: createUniqueFile() is not tested properly
|
||||
storageNew = mainStorage.createUniqueFile(finalFilename, mime);
|
||||
if (storageNew == null)
|
||||
showFailedDialog(R.string.error_file_creation);
|
||||
else
|
||||
continueSelectedDownload(storageNew);
|
||||
break;
|
||||
}
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.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 psName = null;
|
||||
String[] psArgs = null;
|
||||
String secondaryStreamUrl = null;
|
||||
long nearLength = 0;
|
||||
|
||||
if (selectedStream instanceof AudioStream) {
|
||||
// more download logic: select muxer, subtitle converter, etc.
|
||||
switch (radioStreamsGroup.getCheckedRadioButtonId()) {
|
||||
case R.id.audio_button:
|
||||
threads = 1;// use unique thread for subtitles due small file size
|
||||
kind = 'a';
|
||||
selectedStream = audioStreamsAdapter.getItem(selectedAudioIndex);
|
||||
|
||||
if (selectedStream.getFormat() == MediaFormat.M4A) {
|
||||
psName = Postprocessing.ALGORITHM_M4A_NO_DASH;
|
||||
}
|
||||
} else if (selectedStream instanceof VideoStream) {
|
||||
break;
|
||||
case R.id.video_button:
|
||||
kind = 'v';
|
||||
selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex);
|
||||
|
||||
SecondaryStreamHelper<AudioStream> secondaryStream = videoStreamsAdapter
|
||||
.getAllSecondary()
|
||||
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
|
||||
|
||||
if (secondaryStream != null) {
|
||||
secondaryStreamUrl = secondaryStream.getStream().getUrl();
|
||||
psName = selectedStream.getFormat() == MediaFormat.MPEG_4 ? Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER : Postprocessing.ALGORITHM_WEBM_MUXER;
|
||||
|
||||
if (selectedStream.getFormat() == MediaFormat.MPEG_4)
|
||||
psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER;
|
||||
else
|
||||
psName = Postprocessing.ALGORITHM_WEBM_MUXER;
|
||||
|
||||
psArgs = null;
|
||||
long videoSize = wrappedVideoStreams.getSizeInBytes((VideoStream) selectedStream);
|
||||
|
||||
|
@ -542,7 +727,12 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
nearLength = secondaryStream.getSizeInBytes() + videoSize;
|
||||
}
|
||||
}
|
||||
} else if ((selectedStream instanceof SubtitlesStream) && selectedStream.getFormat() == MediaFormat.TTML) {
|
||||
break;
|
||||
case R.id.subtitle_button:
|
||||
kind = 's';
|
||||
selectedStream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
|
||||
|
||||
if (selectedStream.getFormat() == MediaFormat.TTML) {
|
||||
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
|
||||
psArgs = new String[]{
|
||||
selectedStream.getFormat().getSuffix(),
|
||||
|
@ -550,6 +740,10 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
"false",// detect youtube duplicate lines
|
||||
};
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
if (secondaryStreamUrl == null) {
|
||||
urls = new String[]{selectedStream.getUrl()};
|
||||
|
@ -557,8 +751,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||
urls = new String[]{selectedStream.getUrl(), secondaryStreamUrl};
|
||||
}
|
||||
|
||||
DownloadManagerService.startMission(context, urls, location, fileName, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength);
|
||||
DownloadManagerService.startMission(context, urls, storage, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength);
|
||||
|
||||
getDialog().dismiss();
|
||||
dismiss();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,8 +21,8 @@ import org.schabi.newpipe.extractor.NewPipe;
|
|||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.fragments.list.BaseListFragment;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionService;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
|
@ -262,7 +262,7 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi
|
|||
* If chosen feed already displayed, then we request another feed from another
|
||||
* subscription, until the subscription table runs out of new items.
|
||||
* <p>
|
||||
* This Observer is self-contained and will dispose itself when complete. However, this
|
||||
* This Observer is self-contained and will close itself when complete. However, this
|
||||
* does not obey the fragment lifecycle and may continue running in the background
|
||||
* until it is complete. This is done due to RxJava2 no longer propagate errors once
|
||||
* an observer is unsubscribed while the thread process is still running.
|
||||
|
|
|
@ -158,7 +158,7 @@ public class MediaSourceManager {
|
|||
* Dispose the manager and releases all message buses and loaders.
|
||||
* */
|
||||
public void dispose() {
|
||||
if (DEBUG) Log.d(TAG, "dispose() called.");
|
||||
if (DEBUG) Log.d(TAG, "close() called.");
|
||||
|
||||
debouncedSignal.onComplete();
|
||||
debouncedLoader.dispose();
|
||||
|
|
|
@ -2,26 +2,42 @@ package org.schabi.newpipe.settings;
|
|||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.RequiresApi;
|
||||
import android.support.annotation.StringRes;
|
||||
import android.support.v7.preference.Preference;
|
||||
import android.util.Log;
|
||||
|
||||
import com.nononsenseapps.filepicker.Utils;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
|
||||
import us.shandian.giga.io.StoredDirectoryHelper;
|
||||
|
||||
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 String DOWNLOAD_PATH_PREFERENCE;
|
||||
private String DOWNLOAD_PATH_VIDEO_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
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
@ -33,16 +49,100 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
|
|||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
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() {
|
||||
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_STORAGE_API = getString(R.string.downloads_storage_api);
|
||||
DOWNLOAD_STORAGE_API_DEFAULT = getString(R.string.downloads_storage_api_default);
|
||||
}
|
||||
|
||||
private void updatePreferencesSummary() {
|
||||
findPreference(DOWNLOAD_PATH_PREFERENCE).setSummary(defaultPreferences.getString(DOWNLOAD_PATH_PREFERENCE, getString(R.string.download_path_summary)));
|
||||
findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE).setSummary(defaultPreferences.getString(DOWNLOAD_PATH_AUDIO_PREFERENCE, getString(R.string.download_path_audio_summary)));
|
||||
prefPathVideo.setSummary(
|
||||
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
|
||||
|
@ -51,17 +151,31 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
|
|||
Log.d(TAG, "onPreferenceTreeClick() called with: preference = [" + preference + "]");
|
||||
}
|
||||
|
||||
if (preference.getKey().equals(DOWNLOAD_PATH_PREFERENCE)
|
||||
|| preference.getKey().equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
|
||||
Intent i = new Intent(getActivity(), FilePickerActivityHelper.class)
|
||||
String key = preference.getKey();
|
||||
|
||||
if (key.equals(DOWNLOAD_PATH_VIDEO_PREFERENCE) || key.equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
|
||||
boolean safPick = !usingJavaIO();
|
||||
|
||||
int request = 0;
|
||||
if (key.equals(DOWNLOAD_PATH_VIDEO_PREFERENCE)) {
|
||||
request = REQUEST_DOWNLOAD_VIDEO_PATH;
|
||||
} else if (key.equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
|
||||
request = REQUEST_DOWNLOAD_AUDIO_PATH;
|
||||
}
|
||||
|
||||
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);
|
||||
if (preference.getKey().equals(DOWNLOAD_PATH_PREFERENCE)) {
|
||||
startActivityForResult(i, REQUEST_DOWNLOAD_PATH);
|
||||
} else if (preference.getKey().equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
|
||||
startActivityForResult(i, REQUEST_DOWNLOAD_AUDIO_PATH);
|
||||
}
|
||||
|
||||
startActivityForResult(i, request);
|
||||
}
|
||||
|
||||
return super.onPreferenceTreeClick(preference);
|
||||
|
@ -71,25 +185,50 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
|
|||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onActivityResult() called with: requestCode = [" + requestCode + "], resultCode = [" + resultCode + "], data = [" + data + "]");
|
||||
Log.d(TAG, "onActivityResult() called with: requestCode = [" + requestCode + "], " +
|
||||
"resultCode = [" + resultCode + "], data = [" + data + "]"
|
||||
);
|
||||
}
|
||||
|
||||
if ((requestCode == REQUEST_DOWNLOAD_PATH || requestCode == REQUEST_DOWNLOAD_AUDIO_PATH)
|
||||
&& resultCode == Activity.RESULT_OK && data.getData() != null) {
|
||||
String key = getString(requestCode == REQUEST_DOWNLOAD_PATH ? R.string.download_path_key : R.string.download_path_audio_key);
|
||||
String path = Utils.getFileForUri(data.getData()).getAbsolutePath();
|
||||
if (resultCode != Activity.RESULT_OK) return;
|
||||
|
||||
defaultPreferences.edit().putString(key, path).apply();
|
||||
String key;
|
||||
if (requestCode == REQUEST_DOWNLOAD_VIDEO_PATH)
|
||||
key = DOWNLOAD_PATH_VIDEO_PREFERENCE;
|
||||
else if (requestCode == REQUEST_DOWNLOAD_AUDIO_PATH)
|
||||
key = DOWNLOAD_PATH_AUDIO_PREFERENCE;
|
||||
else
|
||||
return;
|
||||
|
||||
Uri uri = data.getData();
|
||||
if (uri == null) {
|
||||
showMessageDialog(R.string.general_error, R.string.invalid_directory);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!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();
|
||||
|
||||
File target = new File(path);
|
||||
if (!target.canWrite()) {
|
||||
AlertDialog.Builder msg = new AlertDialog.Builder(getContext());
|
||||
msg.setTitle(R.string.download_to_sdcard_error_title);
|
||||
msg.setMessage(R.string.download_to_sdcard_error_message);
|
||||
msg.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> { });
|
||||
msg.show();
|
||||
}
|
||||
File target = new File(URI.create(uri.toString()));
|
||||
if (!target.canWrite())
|
||||
showMessageDialog(R.string.download_to_sdcard_error_title, R.string.download_to_sdcard_error_message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,37 +70,23 @@ public class NewPipeSettings {
|
|||
getAudioDownloadFolder(context);
|
||||
}
|
||||
|
||||
public static File getVideoDownloadFolder(Context context) {
|
||||
return getDir(context, R.string.download_path_key, Environment.DIRECTORY_MOVIES);
|
||||
private static void getVideoDownloadFolder(Context context) {
|
||||
getDir(context, R.string.download_path_video_key, Environment.DIRECTORY_MOVIES);
|
||||
}
|
||||
|
||||
public static String getVideoDownloadPath(Context context) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
final String key = context.getString(R.string.download_path_key);
|
||||
return prefs.getString(key, Environment.DIRECTORY_MOVIES);
|
||||
private static void getAudioDownloadFolder(Context context) {
|
||||
getDir(context, R.string.download_path_audio_key, Environment.DIRECTORY_MUSIC);
|
||||
}
|
||||
|
||||
public static File getAudioDownloadFolder(Context context) {
|
||||
return getDir(context, R.string.download_path_audio_key, Environment.DIRECTORY_MUSIC);
|
||||
}
|
||||
|
||||
public static String getAudioDownloadPath(Context context) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
final String key = context.getString(R.string.download_path_audio_key);
|
||||
return prefs.getString(key, Environment.DIRECTORY_MUSIC);
|
||||
}
|
||||
|
||||
private static File getDir(Context context, int keyID, String defaultDirectoryName) {
|
||||
private static void getDir(Context context, int keyID, String defaultDirectoryName) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
final String key = context.getString(keyID);
|
||||
String downloadPath = prefs.getString(key, null);
|
||||
if ((downloadPath != null) && (!downloadPath.isEmpty())) return new File(downloadPath.trim());
|
||||
if ((downloadPath != null) && (!downloadPath.isEmpty())) return;
|
||||
|
||||
final File dir = getDir(defaultDirectoryName);
|
||||
SharedPreferences.Editor spEditor = prefs.edit();
|
||||
spEditor.putString(key, getNewPipeChildFolderPathForDir(dir));
|
||||
spEditor.putString(key, getNewPipeChildFolderPathForDir(getDir(defaultDirectoryName)));
|
||||
spEditor.apply();
|
||||
return dir;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
@ -110,8 +96,13 @@ public class NewPipeSettings {
|
|||
|
||||
public static void resetDownloadFolders(Context 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_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) {
|
||||
|
|
|
@ -120,7 +120,7 @@ public class Mp4FromDashWriter {
|
|||
parsed = true;
|
||||
|
||||
for (SharpStream src : sourceTracks) {
|
||||
src.dispose();
|
||||
src.close();
|
||||
}
|
||||
|
||||
tracks = null;
|
||||
|
|
|
@ -107,7 +107,7 @@ public class WebMWriter {
|
|||
parsed = true;
|
||||
|
||||
for (SharpStream src : sourceTracks) {
|
||||
src.dispose();
|
||||
src.close();
|
||||
}
|
||||
|
||||
sourceTracks = null;
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
package org.schabi.newpipe.streams.io;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* based on c#
|
||||
*/
|
||||
public abstract class SharpStream {
|
||||
public abstract class SharpStream implements Closeable {
|
||||
|
||||
public abstract int read() throws IOException;
|
||||
|
||||
|
@ -19,9 +20,10 @@ public abstract class SharpStream {
|
|||
|
||||
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();
|
||||
|
||||
|
@ -54,4 +56,8 @@ public abstract class SharpStream {
|
|||
public void seek(long offset) throws IOException {
|
||||
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.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.nio.channels.ClosedByInterruptException;
|
||||
|
||||
|
@ -111,34 +111,10 @@ public class DownloadInitializer extends Thread {
|
|||
if (!mMission.running || Thread.interrupted()) return;
|
||||
}
|
||||
|
||||
File file;
|
||||
if (mMission.current == 0) {
|
||||
file = new File(mMission.location);
|
||||
if (!Utility.mkdir(file, true)) {
|
||||
mMission.notifyError(DownloadMission.ERROR_PATH_CREATION, null);
|
||||
return;
|
||||
}
|
||||
|
||||
file = new File(file, mMission.name);
|
||||
|
||||
// if the name is used by another process, delete it
|
||||
if (file.exists() && !file.isFile() && !file.delete()) {
|
||||
mMission.notifyError(DownloadMission.ERROR_FILE_CREATION, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file.exists() && !file.createNewFile()) {
|
||||
mMission.notifyError(DownloadMission.ERROR_FILE_CREATION, null);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
file = new File(mMission.location, mMission.name);
|
||||
}
|
||||
|
||||
RandomAccessFile af = new RandomAccessFile(file, "rw");
|
||||
af.setLength(mMission.offsets[mMission.current] + mMission.length);
|
||||
af.seek(mMission.offsets[mMission.current]);
|
||||
af.close();
|
||||
SharpStream fs = mMission.storage.getStream();
|
||||
fs.setLength(mMission.offsets[mMission.current] + mMission.length);
|
||||
fs.seek(mMission.offsets[mMission.current]);
|
||||
fs.close();
|
||||
|
||||
if (!mMission.running || Thread.interrupted()) return;
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ package us.shandian.giga.get;
|
|||
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
|
@ -17,6 +18,7 @@ import java.util.List;
|
|||
|
||||
import javax.net.ssl.SSLException;
|
||||
|
||||
import us.shandian.giga.io.StoredFileHelper;
|
||||
import us.shandian.giga.postprocessing.Postprocessing;
|
||||
import us.shandian.giga.service.DownloadManagerService;
|
||||
import us.shandian.giga.util.Utility;
|
||||
|
@ -24,7 +26,7 @@ import us.shandian.giga.util.Utility;
|
|||
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||
|
||||
public class DownloadMission extends Mission {
|
||||
private static final long serialVersionUID = 3L;// last bump: 8 november 2018
|
||||
private static final long serialVersionUID = 4L;// last bump: 27 march 2019
|
||||
|
||||
static final int BUFFER_SIZE = 64 * 1024;
|
||||
final static int BLOCK_SIZE = 512 * 1024;
|
||||
|
@ -43,6 +45,7 @@ public class DownloadMission extends Mission {
|
|||
public static final int ERROR_POSTPROCESSING_STOPPED = 1008;
|
||||
public static final int ERROR_POSTPROCESSING_HOLD = 1009;
|
||||
public static final int ERROR_INSUFFICIENT_STORAGE = 1010;
|
||||
public static final int ERROR_PROGRESS_LOST = 1011;
|
||||
public static final int ERROR_HTTP_NO_CONTENT = 204;
|
||||
public static final int ERROR_HTTP_UNSUPPORTED_RANGE = 206;
|
||||
|
||||
|
@ -71,16 +74,6 @@ public class DownloadMission extends Mission {
|
|||
*/
|
||||
public long[] offsets;
|
||||
|
||||
/**
|
||||
* The post-processing algorithm arguments
|
||||
*/
|
||||
public String[] postprocessingArgs;
|
||||
|
||||
/**
|
||||
* The post-processing algorithm name
|
||||
*/
|
||||
public String postprocessingName;
|
||||
|
||||
/**
|
||||
* Indicates if the post-processing state:
|
||||
* 0: ready
|
||||
|
@ -88,12 +81,12 @@ public class DownloadMission extends Mission {
|
|||
* 2: completed
|
||||
* 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]}
|
||||
|
@ -138,36 +131,23 @@ public class DownloadMission extends Mission {
|
|||
public transient volatile Thread[] threads = new Thread[0];
|
||||
private transient Thread init = null;
|
||||
|
||||
|
||||
protected DownloadMission() {
|
||||
|
||||
}
|
||||
|
||||
public DownloadMission(String url, String name, String location, char kind) {
|
||||
this(new String[]{url}, name, location, kind, null, null);
|
||||
}
|
||||
|
||||
public DownloadMission(String[] urls, String name, String location, char kind, String postprocessingName, String[] postprocessingArgs) {
|
||||
if (name == null) throw new NullPointerException("name is null");
|
||||
if (name.isEmpty()) throw new IllegalArgumentException("name is empty");
|
||||
public DownloadMission(String[] urls, StoredFileHelper storage, char kind, Postprocessing psInstance) {
|
||||
if (urls == null) throw new NullPointerException("urls is null");
|
||||
if (urls.length < 1) throw new IllegalArgumentException("urls is empty");
|
||||
if (location == null) throw new NullPointerException("location is null");
|
||||
if (location.isEmpty()) throw new IllegalArgumentException("location is empty");
|
||||
this.urls = urls;
|
||||
this.name = name;
|
||||
this.location = location;
|
||||
this.kind = kind;
|
||||
this.offsets = new long[urls.length];
|
||||
this.enqueued = true;
|
||||
this.maxRetry = 3;
|
||||
this.storage = storage;
|
||||
|
||||
if (postprocessingName != null) {
|
||||
Postprocessing algorithm = Postprocessing.getAlgorithm(postprocessingName, null);
|
||||
this.postprocessingThis = algorithm.worksOnSameFile;
|
||||
this.offsets[0] = algorithm.recommendedReserve;
|
||||
this.postprocessingName = postprocessingName;
|
||||
this.postprocessingArgs = postprocessingArgs;
|
||||
if (psInstance != null) {
|
||||
this.psAlgorithm = psInstance;
|
||||
this.offsets[0] = psInstance.recommendedReserve;
|
||||
} else {
|
||||
if (DEBUG && urls.length > 1) {
|
||||
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);
|
||||
|
||||
if (err instanceof IOException) {
|
||||
if (err.getMessage().contains("Permission denied")) {
|
||||
if (storage.canWrite() || err.getMessage().contains("Permission denied")) {
|
||||
code = ERROR_PERMISSION_DENIED;
|
||||
err = null;
|
||||
} else if (err.getMessage().contains("write failed: ENOSPC")) {
|
||||
} else if (err.getMessage().contains("ENOSPC")) {
|
||||
code = ERROR_INSUFFICIENT_STORAGE;
|
||||
err = null;
|
||||
} else {
|
||||
try {
|
||||
File storage = new File(location);
|
||||
if (storage.canWrite() && storage.getUsableSpace() < (getLength() - done)) {
|
||||
code = ERROR_INSUFFICIENT_STORAGE;
|
||||
err = null;
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
// is a permission error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -433,11 +403,11 @@ public class DownloadMission extends Mission {
|
|||
action = "Failed";
|
||||
}
|
||||
|
||||
Log.d(TAG, action + " postprocessing on " + location + File.separator + name);
|
||||
Log.d(TAG, action + " postprocessing on " + storage.getName());
|
||||
|
||||
synchronized (blockState) {
|
||||
// don't return without fully write the current state
|
||||
postprocessingState = state;
|
||||
psState = state;
|
||||
Utility.writeToFile(metadata, DownloadMission.this);
|
||||
}
|
||||
}
|
||||
|
@ -456,7 +426,7 @@ public class DownloadMission extends Mission {
|
|||
running = true;
|
||||
errCode = ERROR_NOTHING;
|
||||
|
||||
if (current >= urls.length && postprocessingName != null) {
|
||||
if (current >= urls.length && psAlgorithm != null) {
|
||||
runAsync(1, () -> {
|
||||
if (doPostprocessing()) {
|
||||
running = false;
|
||||
|
@ -593,7 +563,7 @@ public class DownloadMission extends Mission {
|
|||
* @return true, otherwise, false
|
||||
*/
|
||||
public boolean isFinished() {
|
||||
return current >= urls.length && (postprocessingName == null || postprocessingState == 2);
|
||||
return current >= urls.length && (psAlgorithm == null || psState == 2);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -602,7 +572,13 @@ public class DownloadMission extends Mission {
|
|||
* @return {@code true} if this mission is unrecoverable
|
||||
*/
|
||||
public boolean isPsFailed() {
|
||||
return postprocessingName != null && errCode == DownloadMission.ERROR_POSTPROCESSING && postprocessingThis;
|
||||
switch (errCode) {
|
||||
case ERROR_POSTPROCESSING:
|
||||
case ERROR_POSTPROCESSING_STOPPED:
|
||||
return psAlgorithm.worksOnSameFile;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -611,7 +587,7 @@ public class DownloadMission extends Mission {
|
|||
* @return true, otherwise, false
|
||||
*/
|
||||
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() {
|
||||
long calculated;
|
||||
if (postprocessingState == 1 || postprocessingState == 3) {
|
||||
if (psState == 1 || psState == 3) {
|
||||
calculated = length;
|
||||
} else {
|
||||
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
|
||||
*/
|
||||
public void psContinue(boolean recover) {
|
||||
postprocessingState = 1;
|
||||
psState = 1;
|
||||
errCode = recover ? ERROR_NOTHING : ERROR_POSTPROCESSING;
|
||||
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() {
|
||||
if (postprocessingName == null || postprocessingState == 2) return true;
|
||||
if (psAlgorithm == null || psState == 2) return true;
|
||||
|
||||
notifyPostProcessing(1);
|
||||
notifyProgress(0);
|
||||
|
||||
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()};
|
||||
|
||||
Exception exception = null;
|
||||
|
||||
try {
|
||||
Postprocessing
|
||||
.getAlgorithm(postprocessingName, this)
|
||||
.run();
|
||||
psAlgorithm.run(this);
|
||||
} catch (Exception err) {
|
||||
StringBuilder args = new StringBuilder(" ");
|
||||
if (postprocessingArgs != null) {
|
||||
for (String arg : postprocessingArgs) {
|
||||
args.append(", ");
|
||||
args.append(arg);
|
||||
}
|
||||
args.delete(0, 1);
|
||||
}
|
||||
Log.e(TAG, String.format("Post-processing failed. algorithm = %s args = [%s]", postprocessingName, args), err);
|
||||
Log.e(TAG, "Post-processing failed. " + psAlgorithm.toString(), err);
|
||||
|
||||
if (errCode == ERROR_NOTHING) errCode = ERROR_POSTPROCESSING;
|
||||
|
||||
|
@ -733,7 +731,7 @@ public class DownloadMission extends Mission {
|
|||
// >=1: any download thread
|
||||
|
||||
if (DEBUG) {
|
||||
who.setName(String.format("%s[%s] %s", TAG, id, name));
|
||||
who.setName(String.format("%s[%s] %s", TAG, id, storage.getName()));
|
||||
}
|
||||
|
||||
who.start();
|
||||
|
|
|
@ -2,9 +2,10 @@ package us.shandian.giga.get;
|
|||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.nio.channels.ClosedByInterruptException;
|
||||
|
||||
|
@ -40,12 +41,12 @@ public class DownloadRunnable extends Thread {
|
|||
Log.d(TAG, mId + ":recovered: " + mMission.recovered);
|
||||
}
|
||||
|
||||
RandomAccessFile f;
|
||||
SharpStream f;
|
||||
InputStream is = null;
|
||||
|
||||
try {
|
||||
f = new RandomAccessFile(mMission.getDownloadedFile(), "rw");
|
||||
} catch (FileNotFoundException e) {
|
||||
f = mMission.storage.getStream();
|
||||
} catch (IOException e) {
|
||||
mMission.notifyError(e);// this never should happen
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -4,13 +4,13 @@ import android.annotation.SuppressLint;
|
|||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.nio.channels.ClosedByInterruptException;
|
||||
|
||||
|
||||
import us.shandian.giga.util.Utility;
|
||||
|
||||
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||
|
@ -22,11 +22,10 @@ public class DownloadRunnableFallback extends Thread {
|
|||
private static final String TAG = "DownloadRunnableFallback";
|
||||
|
||||
private final DownloadMission mMission;
|
||||
private final int mId = 1;
|
||||
|
||||
private int mRetryCount = 0;
|
||||
private InputStream mIs;
|
||||
private RandomAccessFile mF;
|
||||
private SharpStream mF;
|
||||
private HttpURLConnection mConn;
|
||||
|
||||
DownloadRunnableFallback(@NonNull DownloadMission mission) {
|
||||
|
@ -43,11 +42,7 @@ public class DownloadRunnableFallback extends Thread {
|
|||
// nothing to do
|
||||
}
|
||||
|
||||
try {
|
||||
if (mF != null) mF.close();
|
||||
} catch (IOException e) {
|
||||
// ¿ejected media storage? ¿file deleted? ¿storage ran out of space?
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -67,6 +62,7 @@ public class DownloadRunnableFallback extends Thread {
|
|||
try {
|
||||
long rangeStart = (mMission.unknownLength || start < 1) ? -1 : start;
|
||||
|
||||
int mId = 1;
|
||||
mConn = mMission.openConnection(mId, rangeStart, -1);
|
||||
mMission.establishConnection(mId, mConn);
|
||||
|
||||
|
@ -81,7 +77,7 @@ public class DownloadRunnableFallback extends Thread {
|
|||
if (!mMission.unknownLength)
|
||||
mMission.unknownLength = Utility.getContentLength(mConn) == -1;
|
||||
|
||||
mF = new RandomAccessFile(mMission.getDownloadedFile(), "rw");
|
||||
mF = mMission.storage.getStream();
|
||||
mF.seek(mMission.offsets[mMission.current] + start);
|
||||
|
||||
mIs = mConn.getInputStream();
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
package us.shandian.giga.get;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
public class FinishedMission extends Mission {
|
||||
|
||||
public FinishedMission() {
|
||||
}
|
||||
|
||||
public FinishedMission(DownloadMission mission) {
|
||||
public FinishedMission(@NonNull DownloadMission mission) {
|
||||
source = mission.source;
|
||||
length = mission.length;// ¿or mission.done?
|
||||
timestamp = mission.timestamp;
|
||||
name = mission.name;
|
||||
location = mission.location;
|
||||
kind = mission.kind;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
package us.shandian.giga.get;
|
||||
|
||||
import java.io.File;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
|
||||
import us.shandian.giga.io.StoredFileHelper;
|
||||
|
||||
public abstract class Mission implements Serializable {
|
||||
private static final long serialVersionUID = 0L;// last bump: 5 october 2018
|
||||
private static final long serialVersionUID = 1L;// last bump: 27 march 2019
|
||||
|
||||
/**
|
||||
* Source url of the resource
|
||||
|
@ -23,28 +26,23 @@ public abstract class Mission implements Serializable {
|
|||
*/
|
||||
public long timestamp;
|
||||
|
||||
/**
|
||||
* The filename
|
||||
*/
|
||||
public String name;
|
||||
|
||||
/**
|
||||
* The directory to store the download
|
||||
*/
|
||||
public String location;
|
||||
|
||||
/**
|
||||
* pre-defined content type
|
||||
*/
|
||||
public char kind;
|
||||
|
||||
/**
|
||||
* The downloaded file
|
||||
*/
|
||||
public StoredFileHelper storage;
|
||||
|
||||
/**
|
||||
* get the target file on the storage
|
||||
*
|
||||
* @return File object
|
||||
*/
|
||||
public File getDownloadedFile() {
|
||||
return new File(location, name);
|
||||
public Uri getDownloadedFileUri() {
|
||||
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}
|
||||
*/
|
||||
public boolean delete() {
|
||||
deleted = true;
|
||||
return getDownloadedFile().delete();
|
||||
if (storage != null) return storage.delete();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -62,10 +60,11 @@ public abstract class Mission implements Serializable {
|
|||
*/
|
||||
public transient boolean deleted = false;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTimeInMillis(timestamp);
|
||||
return "[" + calendar.getTime().toString() + "] " + location + File.separator + name;
|
||||
return "[" + calendar.getTime().toString() + "] " + 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,20 +1,22 @@
|
|||
package us.shandian.giga.postprocessing.io;
|
||||
package us.shandian.giga.io;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
|
||||
public class ChunkFileInputStream extends SharpStream {
|
||||
|
||||
private RandomAccessFile source;
|
||||
private SharpStream source;
|
||||
private final long offset;
|
||||
private final long length;
|
||||
private long position;
|
||||
|
||||
public ChunkFileInputStream(File file, long start, long end, String mode) throws IOException {
|
||||
source = new RandomAccessFile(file, mode);
|
||||
public ChunkFileInputStream(SharpStream target, long start) throws IOException {
|
||||
this(target, start, target.length());
|
||||
}
|
||||
|
||||
public ChunkFileInputStream(SharpStream target, long start, long end) throws IOException {
|
||||
source = target;
|
||||
offset = start;
|
||||
length = end - start;
|
||||
position = 0;
|
||||
|
@ -100,17 +102,13 @@ public class ChunkFileInputStream extends SharpStream {
|
|||
|
||||
@SuppressWarnings("EmptyCatchBlock")
|
||||
@Override
|
||||
public void dispose() {
|
||||
try {
|
||||
public void close() {
|
||||
source.close();
|
||||
} catch (IOException err) {
|
||||
} finally {
|
||||
source = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDisposed() {
|
||||
public boolean isClosed() {
|
||||
return source == null;
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package us.shandian.giga.postprocessing.io;
|
||||
package us.shandian.giga.io;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
|
@ -7,7 +7,6 @@ import org.schabi.newpipe.streams.io.SharpStream;
|
|||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
|
||||
public class CircularFileWriter extends SharpStream {
|
||||
|
||||
|
@ -26,7 +25,7 @@ public class CircularFileWriter extends SharpStream {
|
|||
private BufferedFile out;
|
||||
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) {
|
||||
throw new NullPointerException("checker is null");
|
||||
}
|
||||
|
@ -38,7 +37,7 @@ public class CircularFileWriter extends SharpStream {
|
|||
}
|
||||
|
||||
aux = new BufferedFile(temp);
|
||||
out = new BufferedFile(source);
|
||||
out = new BufferedFile(target);
|
||||
|
||||
callback = checker;
|
||||
|
||||
|
@ -105,7 +104,7 @@ public class CircularFileWriter extends SharpStream {
|
|||
out.target.setLength(length);
|
||||
}
|
||||
|
||||
dispose();
|
||||
close();
|
||||
|
||||
return length;
|
||||
}
|
||||
|
@ -114,13 +113,13 @@ public class CircularFileWriter extends SharpStream {
|
|||
* Close the file without flushing any buffer
|
||||
*/
|
||||
@Override
|
||||
public void dispose() {
|
||||
public void close() {
|
||||
if (out != null) {
|
||||
out.dispose();
|
||||
out.close();
|
||||
out = null;
|
||||
}
|
||||
if (aux != null) {
|
||||
aux.dispose();
|
||||
aux.close();
|
||||
aux = null;
|
||||
}
|
||||
}
|
||||
|
@ -256,7 +255,7 @@ public class CircularFileWriter extends SharpStream {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean isDisposed() {
|
||||
public boolean isClosed() {
|
||||
return out == null;
|
||||
}
|
||||
|
||||
|
@ -339,30 +338,29 @@ public class CircularFileWriter extends SharpStream {
|
|||
|
||||
class BufferedFile {
|
||||
|
||||
protected final RandomAccessFile target;
|
||||
protected final SharpStream target;
|
||||
|
||||
private long offset;
|
||||
protected long length;
|
||||
|
||||
private byte[] queue;
|
||||
private byte[] queue = new byte[QUEUE_BUFFER_SIZE];
|
||||
private int queueSize;
|
||||
|
||||
BufferedFile(File file) throws FileNotFoundException {
|
||||
queue = new byte[QUEUE_BUFFER_SIZE];
|
||||
target = new RandomAccessFile(file, "rw");
|
||||
this.target = new FileStream(file);
|
||||
}
|
||||
|
||||
BufferedFile(SharpStream target) {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
protected long getOffset() {
|
||||
return offset + queueSize;// absolute offset in the file
|
||||
}
|
||||
|
||||
protected void dispose() {
|
||||
try {
|
||||
protected void close() {
|
||||
queue = null;
|
||||
target.close();
|
||||
} catch (IOException e) {
|
||||
// nothing to do
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
offset += queueSize;
|
||||
queueSize = 0;
|
||||
|
@ -404,7 +402,7 @@ public class CircularFileWriter extends SharpStream {
|
|||
return queue.length - queueSize;
|
||||
}
|
||||
|
||||
protected void reset() throws IOException {
|
||||
void reset() throws IOException {
|
||||
offset = 0;
|
||||
length = 0;
|
||||
target.seek(0);
|
||||
|
@ -415,7 +413,7 @@ public class CircularFileWriter extends SharpStream {
|
|||
target.seek(absoluteOffset);
|
||||
}
|
||||
|
||||
protected void writeProof(byte[] buffer, int length) throws IOException {
|
||||
void writeProof(byte[] buffer, int length) throws IOException {
|
||||
if (onWriteError == null) {
|
||||
target.write(buffer, 0, length);
|
||||
return;
|
||||
|
@ -436,14 +434,8 @@ public class CircularFileWriter extends SharpStream {
|
|||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
String absOffset;
|
||||
String absLength;
|
||||
|
||||
try {
|
||||
absOffset = Long.toString(target.getFilePointer());
|
||||
} catch (IOException e) {
|
||||
absOffset = "[" + e.getLocalizedMessage() + "]";
|
||||
}
|
||||
try {
|
||||
absLength = Long.toString(target.length());
|
||||
} catch (IOException e) {
|
||||
|
@ -451,8 +443,8 @@ public class CircularFileWriter extends SharpStream {
|
|||
}
|
||||
|
||||
return String.format(
|
||||
"offset=%s length=%s queue=%s absOffset=%s absLength=%s",
|
||||
offset, length, queueSize, absOffset, absLength
|
||||
"offset=%s length=%s queue=%s absLength=%s",
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
* To change this template file, choose Tools | Templates
|
||||
* and open the template in the editor.
|
||||
*/
|
||||
package us.shandian.giga.postprocessing.io;
|
||||
package us.shandian.giga.io;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
|
@ -56,6 +56,6 @@ public class SharpInputStream extends InputStream {
|
|||
|
||||
@Override
|
||||
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 us.shandian.giga.get.DownloadMission;
|
||||
|
||||
public class M4aNoDash extends Postprocessing {
|
||||
|
||||
M4aNoDash(DownloadMission mission) {
|
||||
super(mission, 0, true);
|
||||
M4aNoDash() {
|
||||
super(0, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -5,15 +5,13 @@ import org.schabi.newpipe.streams.io.SharpStream;
|
|||
|
||||
import java.io.IOException;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
|
||||
/**
|
||||
* @author kapodamy
|
||||
*/
|
||||
class Mp4FromDashMuxer extends Postprocessing {
|
||||
|
||||
Mp4FromDashMuxer(DownloadMission mission) {
|
||||
super(mission, 2 * 1024 * 1024/* 2 MiB */, true);
|
||||
Mp4FromDashMuxer() {
|
||||
super(2 * 1024 * 1024/* 2 MiB */, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package us.shandian.giga.postprocessing;
|
||||
|
||||
import android.os.Message;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
@ -9,9 +10,9 @@ import java.io.File;
|
|||
import java.io.IOException;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
import us.shandian.giga.postprocessing.io.ChunkFileInputStream;
|
||||
import us.shandian.giga.postprocessing.io.CircularFileWriter;
|
||||
import us.shandian.giga.postprocessing.io.CircularFileWriter.OffsetChecker;
|
||||
import us.shandian.giga.io.ChunkFileInputStream;
|
||||
import us.shandian.giga.io.CircularFileWriter;
|
||||
import us.shandian.giga.io.CircularFileWriter.OffsetChecker;
|
||||
import us.shandian.giga.service.DownloadManagerService;
|
||||
|
||||
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 {
|
||||
|
||||
static final byte OK_RESULT = ERROR_NOTHING;
|
||||
static transient final byte OK_RESULT = ERROR_NOTHING;
|
||||
|
||||
public static final String ALGORITHM_TTML_CONVERTER = "ttml";
|
||||
public static final String ALGORITHM_WEBM_MUXER = "webm";
|
||||
public static final String ALGORITHM_MP4_FROM_DASH_MUXER = "mp4D-mp4";
|
||||
public static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a";
|
||||
public transient static final String ALGORITHM_TTML_CONVERTER = "ttml";
|
||||
public transient static final String ALGORITHM_WEBM_MUXER = "webm";
|
||||
public transient static final String ALGORITHM_MP4_FROM_DASH_MUXER = "mp4D-mp4";
|
||||
public transient static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a";
|
||||
|
||||
public static Postprocessing getAlgorithm(String algorithmName, String[] args) {
|
||||
Postprocessing instance;
|
||||
|
||||
public static Postprocessing getAlgorithm(String algorithmName, DownloadMission mission) {
|
||||
if (null == algorithmName) {
|
||||
throw new NullPointerException("algorithmName");
|
||||
} else switch (algorithmName) {
|
||||
case ALGORITHM_TTML_CONVERTER:
|
||||
return new TtmlConverter(mission);
|
||||
instance = new TtmlConverter();
|
||||
break;
|
||||
case ALGORITHM_WEBM_MUXER:
|
||||
return new WebMMuxer(mission);
|
||||
instance = new WebMMuxer();
|
||||
break;
|
||||
case ALGORITHM_MP4_FROM_DASH_MUXER:
|
||||
return new Mp4FromDashMuxer(mission);
|
||||
instance = new Mp4FromDashMuxer();
|
||||
break;
|
||||
case ALGORITHM_M4A_NO_DASH:
|
||||
return new M4aNoDash(mission);
|
||||
instance = new M4aNoDash();
|
||||
break;
|
||||
/*case "example-algorithm":
|
||||
return new ExampleAlgorithm(mission);*/
|
||||
instance = new ExampleAlgorithm(mission);*/
|
||||
default:
|
||||
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
|
||||
*/
|
||||
protected DownloadMission mission;
|
||||
protected transient DownloadMission mission;
|
||||
|
||||
Postprocessing(DownloadMission mission, int recommendedReserve, boolean worksOnSameFile) {
|
||||
this.mission = mission;
|
||||
public transient File cacheDir;
|
||||
|
||||
private String[] args;
|
||||
|
||||
private String name;
|
||||
|
||||
Postprocessing(int recommendedReserve, boolean worksOnSameFile) {
|
||||
this.recommendedReserve = recommendedReserve;
|
||||
this.worksOnSameFile = worksOnSameFile;
|
||||
}
|
||||
|
||||
public void run() throws IOException {
|
||||
File file = mission.getDownloadedFile();
|
||||
public void run(DownloadMission target) throws IOException {
|
||||
this.mission = target;
|
||||
|
||||
File temp = null;
|
||||
CircularFileWriter out = null;
|
||||
int result;
|
||||
long finalLength = -1;
|
||||
|
||||
mission.done = 0;
|
||||
mission.length = file.length();
|
||||
mission.length = mission.storage.length();
|
||||
|
||||
if (worksOnSameFile) {
|
||||
ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length];
|
||||
try {
|
||||
int i = 0;
|
||||
for (; i < sources.length - 1; i++) {
|
||||
sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.offsets[i + 1], "rw");
|
||||
sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i], mission.offsets[i + 1]);
|
||||
}
|
||||
sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.getDownloadedFile().length(), "rw");
|
||||
sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i]);
|
||||
|
||||
if (test(sources)) {
|
||||
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)
|
||||
* 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
|
||||
}
|
||||
|
||||
|
@ -107,18 +125,19 @@ public abstract class Postprocessing {
|
|||
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.onWriteError = (err) -> {
|
||||
mission.postprocessingState = 3;
|
||||
mission.psState = 3;
|
||||
mission.notifyError(ERROR_POSTPROCESSING_HOLD, err);
|
||||
|
||||
try {
|
||||
synchronized (this) {
|
||||
while (mission.postprocessingState == 3)
|
||||
while (mission.psState == 3)
|
||||
wait();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
|
@ -138,12 +157,12 @@ public abstract class Postprocessing {
|
|||
}
|
||||
} finally {
|
||||
for (SharpStream source : sources) {
|
||||
if (source != null && !source.isDisposed()) {
|
||||
source.dispose();
|
||||
if (source != null && !source.isClosed()) {
|
||||
source.close();
|
||||
}
|
||||
}
|
||||
if (out != null) {
|
||||
out.dispose();
|
||||
out.close();
|
||||
}
|
||||
if (temp != null) {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
|
@ -164,10 +183,9 @@ public abstract class Postprocessing {
|
|||
mission.errObject = new RuntimeException("post-processing algorithm returned " + result);
|
||||
}
|
||||
|
||||
if (result != OK_RESULT && worksOnSameFile) {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
file.delete();
|
||||
}
|
||||
if (result != OK_RESULT && worksOnSameFile) mission.storage.delete();
|
||||
|
||||
this.mission = null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -192,11 +210,11 @@ public abstract class Postprocessing {
|
|||
abstract int process(SharpStream out, SharpStream... sources) throws IOException;
|
||||
|
||||
String getArgumentAt(int index, String defaultValue) {
|
||||
if (mission.postprocessingArgs == null || index >= mission.postprocessingArgs.length) {
|
||||
if (args == null || index >= args.length) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return mission.postprocessingArgs[index];
|
||||
return args[index];
|
||||
}
|
||||
|
||||
private void progressReport(long done) {
|
||||
|
@ -209,4 +227,22 @@ public abstract class Postprocessing {
|
|||
|
||||
mission.mHandler.sendMessage(m);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder str = new StringBuilder();
|
||||
|
||||
str.append("name=").append(name).append('[');
|
||||
|
||||
if (args != null) {
|
||||
for (String arg : args) {
|
||||
str.append(", ");
|
||||
str.append(arg);
|
||||
}
|
||||
str.delete(0, 1);
|
||||
}
|
||||
|
||||
return str.append(']').toString();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@ package us.shandian.giga.postprocessing;
|
|||
|
||||
import android.util.Log;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
import org.schabi.newpipe.streams.SubtitleConverter;
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -12,18 +12,15 @@ import java.text.ParseException;
|
|||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.xml.xpath.XPathExpressionException;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
import us.shandian.giga.postprocessing.io.SharpInputStream;
|
||||
|
||||
/**
|
||||
* @author kapodamy
|
||||
*/
|
||||
class TtmlConverter extends Postprocessing {
|
||||
private static final String TAG = "TtmlConverter";
|
||||
|
||||
TtmlConverter(DownloadMission mission) {
|
||||
TtmlConverter() {
|
||||
// due how XmlPullParser works, the xml is fully loaded on the ram
|
||||
super(mission, 0, true);
|
||||
super(0, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -7,15 +7,13 @@ import org.schabi.newpipe.streams.io.SharpStream;
|
|||
|
||||
import java.io.IOException;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
|
||||
/**
|
||||
* @author kapodamy
|
||||
*/
|
||||
class WebMMuxer extends Postprocessing {
|
||||
|
||||
WebMMuxer(DownloadMission mission) {
|
||||
super(mission, 2048 * 1024/* 2 MiB */, true);
|
||||
WebMMuxer() {
|
||||
super(2048 * 1024/* 2 MiB */, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -13,16 +13,15 @@ import org.schabi.newpipe.R;
|
|||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
import us.shandian.giga.get.FinishedMission;
|
||||
import us.shandian.giga.get.Mission;
|
||||
import us.shandian.giga.get.sqlite.DownloadDataSource;
|
||||
import us.shandian.giga.service.DownloadManagerService.DMChecker;
|
||||
import us.shandian.giga.service.DownloadManagerService.MissionCheck;
|
||||
import us.shandian.giga.get.sqlite.FinishedMissionStore;
|
||||
import us.shandian.giga.io.StoredDirectoryHelper;
|
||||
import us.shandian.giga.io.StoredFileHelper;
|
||||
import us.shandian.giga.util.Utility;
|
||||
|
||||
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||
|
@ -36,7 +35,10 @@ public class DownloadManager {
|
|||
public final static int SPECIAL_PENDING = 1;
|
||||
public final static int SPECIAL_FINISHED = 2;
|
||||
|
||||
private final DownloadDataSource mDownloadDataSource;
|
||||
static final String TAG_AUDIO = "audio";
|
||||
static final String TAG_VIDEO = "video";
|
||||
|
||||
private final FinishedMissionStore mFinishedMissionStore;
|
||||
|
||||
private final ArrayList<DownloadMission> mMissionsPending = new ArrayList<>();
|
||||
private final ArrayList<FinishedMission> mMissionsFinished;
|
||||
|
@ -51,6 +53,9 @@ public class DownloadManager {
|
|||
boolean mPrefQueueLimit;
|
||||
private boolean mSelfMissionsControl;
|
||||
|
||||
StoredDirectoryHelper mMainStorageAudio;
|
||||
StoredDirectoryHelper mMainStorageVideo;
|
||||
|
||||
/**
|
||||
* Create a new instance
|
||||
*
|
||||
|
@ -62,7 +67,7 @@ public class DownloadManager {
|
|||
Log.d(TAG, "new DownloadManager instance. 0x" + Integer.toHexString(this.hashCode()));
|
||||
}
|
||||
|
||||
mDownloadDataSource = new DownloadDataSource(context);
|
||||
mFinishedMissionStore = new FinishedMissionStore(context);
|
||||
mHandler = handler;
|
||||
mMissionsFinished = loadFinishedMissions();
|
||||
mPendingMissionsDir = getPendingDir(context);
|
||||
|
@ -71,7 +76,7 @@ public class DownloadManager {
|
|||
throw new RuntimeException("failed to create pending_downloads in data directory");
|
||||
}
|
||||
|
||||
loadPendingMissions();
|
||||
loadPendingMissions(context);
|
||||
}
|
||||
|
||||
private static File getPendingDir(@NonNull Context context) {
|
||||
|
@ -92,29 +97,24 @@ public class DownloadManager {
|
|||
* Loads finished missions from the data source
|
||||
*/
|
||||
private ArrayList<FinishedMission> loadFinishedMissions() {
|
||||
ArrayList<FinishedMission> finishedMissions = mDownloadDataSource.loadFinishedMissions();
|
||||
ArrayList<FinishedMission> finishedMissions = mFinishedMissionStore.loadFinishedMissions();
|
||||
|
||||
// missions always is stored by creation order, simply reverse the list
|
||||
ArrayList<FinishedMission> result = new ArrayList<>(finishedMissions.size());
|
||||
// check if the files exists, otherwise, forget the download
|
||||
for (int i = finishedMissions.size() - 1; i >= 0; i--) {
|
||||
FinishedMission mission = finishedMissions.get(i);
|
||||
File file = mission.getDownloadedFile();
|
||||
|
||||
if (!file.isFile()) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "downloaded file removed: " + file.getAbsolutePath());
|
||||
if (!mission.storage.existsAsFile()) {
|
||||
if (DEBUG) Log.d(TAG, "downloaded file removed: " + mission.storage.getName());
|
||||
|
||||
mFinishedMissionStore.deleteMission(mission);
|
||||
finishedMissions.remove(i);
|
||||
}
|
||||
mDownloadDataSource.deleteMission(mission);
|
||||
continue;
|
||||
}
|
||||
|
||||
result.add(mission);
|
||||
return finishedMissions;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void loadPendingMissions() {
|
||||
private void loadPendingMissions(Context ctx) {
|
||||
File[] subs = mPendingMissionsDir.listFiles();
|
||||
|
||||
if (subs == null) {
|
||||
|
@ -142,40 +142,63 @@ public class DownloadManager {
|
|||
continue;
|
||||
}
|
||||
|
||||
File dl = mis.getDownloadedFile();
|
||||
boolean exists = dl.exists();
|
||||
boolean exists;
|
||||
try {
|
||||
mis.storage = StoredFileHelper.deserialize(mis.storage, ctx);
|
||||
exists = !mis.storage.isInvalid() && mis.storage.existsAsFile();
|
||||
|
||||
} catch (Exception ex) {
|
||||
Log.e(TAG, "Failed to load the file source of " + mis.storage.toString());
|
||||
mis.storage.invalidate();
|
||||
exists = false;
|
||||
}
|
||||
|
||||
if (mis.isPsRunning()) {
|
||||
if (mis.postprocessingThis) {
|
||||
if (mis.psAlgorithm.worksOnSameFile) {
|
||||
// Incomplete post-processing results in a corrupted download file
|
||||
// because the selected algorithm works on the same file to save space.
|
||||
if (exists && dl.isFile() && !dl.delete())
|
||||
if (exists && !mis.storage.delete())
|
||||
Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath());
|
||||
|
||||
exists = true;
|
||||
}
|
||||
|
||||
mis.postprocessingState = 0;
|
||||
mis.psState = 0;
|
||||
mis.errCode = DownloadMission.ERROR_POSTPROCESSING_STOPPED;
|
||||
mis.errObject = null;
|
||||
} else if (exists && !dl.isFile()) {
|
||||
// probably a folder, this should never happens
|
||||
if (!sub.delete()) {
|
||||
Log.w(TAG, "Unable to delete serialized file: " + sub.getPath());
|
||||
}
|
||||
continue;
|
||||
} else if (!exists) {
|
||||
|
||||
StoredDirectoryHelper mainStorage = getMainStorage(mis.storage.getTag());
|
||||
|
||||
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 (!exists && mis.isInitialized()) {
|
||||
// downloaded file deleted, reset mission state
|
||||
DownloadMission m = new DownloadMission(mis.urls, mis.name, mis.location, mis.kind, mis.postprocessingName, mis.postprocessingArgs);
|
||||
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.setEnqueued(mis.enqueued);
|
||||
m.enqueued = mis.enqueued;
|
||||
m.errCode = DownloadMission.ERROR_PROGRESS_LOST;
|
||||
mis = m;
|
||||
}
|
||||
}
|
||||
|
||||
if (mis.psAlgorithm != null) mis.psAlgorithm.cacheDir = ctx.getCacheDir();
|
||||
|
||||
mis.running = false;
|
||||
mis.recovered = exists;
|
||||
|
@ -196,51 +219,15 @@ public class DownloadManager {
|
|||
/**
|
||||
* Start a new download mission
|
||||
*
|
||||
* @param urls the list of urls to download
|
||||
* @param location the location
|
||||
* @param name the name of the file to create
|
||||
* @param kind type of file (a: audio v: video s: subtitle ?: file-extension defined)
|
||||
* @param threads the number of threads maximal used to download chunks of the file.
|
||||
* @param psName the name of the required post-processing algorithm, or {@code null} to ignore.
|
||||
* @param source source url of the resource
|
||||
* @param psArgs the arguments for the post-processing algorithm.
|
||||
* @param mission the new download mission to add and run (if possible)
|
||||
*/
|
||||
void startMission(String[] urls, String location, String name, char kind, int threads,
|
||||
String source, String psName, String[] psArgs, long nearLength) {
|
||||
void startMission(DownloadMission mission) {
|
||||
synchronized (this) {
|
||||
// check for existing pending download
|
||||
DownloadMission pendingMission = getPendingMission(location, name);
|
||||
|
||||
if (pendingMission != null) {
|
||||
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.threadCount = threads;
|
||||
mission.source = source;
|
||||
mission.mHandler = mHandler;
|
||||
mission.maxRetry = mPrefMaxRetry;
|
||||
mission.nearLength = nearLength;
|
||||
|
||||
// create metadata file
|
||||
while (true) {
|
||||
mission.metadata = new File(mPendingMissionsDir, String.valueOf(mission.timestamp));
|
||||
if (!mission.metadata.isFile() && !mission.metadata.exists()) {
|
||||
|
@ -261,6 +248,14 @@ public class DownloadManager {
|
|||
// Before continue, save the metadata in case the internet connection is not available
|
||||
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;
|
||||
|
||||
if (canDownloadInCurrentNetwork() && start) {
|
||||
|
@ -292,7 +287,7 @@ public class DownloadManager {
|
|||
mMissionsPending.remove(mission);
|
||||
} else if (mission instanceof FinishedMission) {
|
||||
mMissionsFinished.remove(mission);
|
||||
mDownloadDataSource.deleteMission(mission);
|
||||
mFinishedMissionStore.deleteMission(mission);
|
||||
}
|
||||
|
||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED);
|
||||
|
@ -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 name the name
|
||||
* @param storage where the file possible is stored
|
||||
* @return the mission or null if no such mission exists
|
||||
*/
|
||||
@Nullable
|
||||
private DownloadMission getPendingMission(String location, String name) {
|
||||
private DownloadMission getPendingMission(StoredFileHelper storage) {
|
||||
for (DownloadMission mission : mMissionsPending) {
|
||||
if (location.equalsIgnoreCase(mission.location) && name.equalsIgnoreCase(mission.name)) {
|
||||
if (mission.storage.equals(storage)) {
|
||||
return mission;
|
||||
}
|
||||
}
|
||||
|
@ -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 name the name
|
||||
* @param storage where the file possible is stored
|
||||
* @return the mission index or -1 if no such mission exists
|
||||
*/
|
||||
private int getFinishedMissionIndex(String location, String name) {
|
||||
private int getFinishedMissionIndex(StoredFileHelper storage) {
|
||||
for (int i = 0; i < mMissionsFinished.size(); i++) {
|
||||
FinishedMission mission = mMissionsFinished.get(i);
|
||||
if (location.equalsIgnoreCase(mission.location) && name.equalsIgnoreCase(mission.name)) {
|
||||
if (mMissionsFinished.get(i).storage.equals(storage)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
@ -336,12 +346,12 @@ public class DownloadManager {
|
|||
return -1;
|
||||
}
|
||||
|
||||
public Mission getAnyMission(String location, String name) {
|
||||
private Mission getAnyMission(StoredFileHelper storage) {
|
||||
synchronized (this) {
|
||||
Mission mission = getPendingMission(location, name);
|
||||
Mission mission = getPendingMission(storage);
|
||||
if (mission != null) return mission;
|
||||
|
||||
int idx = getFinishedMissionIndex(location, name);
|
||||
int idx = getFinishedMissionIndex(storage);
|
||||
if (idx >= 0) return mMissionsFinished.get(idx);
|
||||
}
|
||||
|
||||
|
@ -382,7 +392,7 @@ public class DownloadManager {
|
|||
|
||||
synchronized (this) {
|
||||
for (DownloadMission mission : mMissionsPending) {
|
||||
if (mission.running || mission.isPsFailed() || mission.isFinished()) continue;
|
||||
if (mission.running || !mission.canDownload()) continue;
|
||||
|
||||
flag = true;
|
||||
mission.start();
|
||||
|
@ -392,58 +402,6 @@ public class DownloadManager {
|
|||
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
|
||||
*
|
||||
|
@ -453,7 +411,7 @@ public class DownloadManager {
|
|||
synchronized (this) {
|
||||
mMissionsPending.remove(mission);
|
||||
mMissionsFinished.add(0, new FinishedMission(mission));
|
||||
mDownloadDataSource.addMission(mission);
|
||||
mFinishedMissionStore.addFinishedMission(mission);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -474,7 +432,8 @@ public class DownloadManager {
|
|||
|
||||
boolean flag = false;
|
||||
for (DownloadMission mission : mMissionsPending) {
|
||||
if (mission.running || !mission.enqueued || mission.isFinished()) continue;
|
||||
if (mission.running || !mission.enqueued || mission.isFinished() || mission.hasInvalidStorage())
|
||||
continue;
|
||||
|
||||
resumeMission(mission);
|
||||
if (mPrefQueueLimit) return true;
|
||||
|
@ -496,7 +455,7 @@ public class DownloadManager {
|
|||
public void forgetFinishedDownloads() {
|
||||
synchronized (this) {
|
||||
for (FinishedMission mission : mMissionsFinished) {
|
||||
mDownloadDataSource.deleteMission(mission);
|
||||
mFinishedMissionStore.deleteMission(mission);
|
||||
}
|
||||
mMissionsFinished.clear();
|
||||
}
|
||||
|
@ -523,7 +482,7 @@ public class DownloadManager {
|
|||
int paused = 0;
|
||||
synchronized (this) {
|
||||
for (DownloadMission mission : mMissionsPending) {
|
||||
if (mission.isFinished() || mission.isPsRunning()) continue;
|
||||
if (!mission.canDownload() || mission.isPsRunning()) continue;
|
||||
|
||||
if (mission.running && isMetered) {
|
||||
paused++;
|
||||
|
@ -565,24 +524,32 @@ public class DownloadManager {
|
|||
), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
void checkForRunningMission(String location, String name, DMChecker check) {
|
||||
MissionCheck result = MissionCheck.None;
|
||||
|
||||
public MissionState checkForExistingMission(StoredFileHelper storage) {
|
||||
synchronized (this) {
|
||||
DownloadMission pending = getPendingMission(location, name);
|
||||
DownloadMission pending = getPendingMission(storage);
|
||||
|
||||
if (pending == null) {
|
||||
if (getFinishedMissionIndex(location, name) >= 0) result = MissionCheck.Finished;
|
||||
if (getFinishedMissionIndex(storage) >= 0) return MissionState.Finished;
|
||||
} else {
|
||||
if (pending.isFinished()) {
|
||||
result = MissionCheck.Finished;// this never should happen (race-condition)
|
||||
return MissionState.Finished;// this never should happen (race-condition)
|
||||
} 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 {
|
||||
|
@ -689,7 +656,7 @@ public class DownloadManager {
|
|||
|
||||
synchronized (DownloadManager.this) {
|
||||
for (DownloadMission mission : mMissionsPending) {
|
||||
if (hidden.contains(mission) || mission.isPsFailed() || mission.isFinished())
|
||||
if (hidden.contains(mission) || mission.canDownload())
|
||||
continue;
|
||||
|
||||
if (mission.running)
|
||||
|
@ -720,7 +687,14 @@ public class DownloadManager {
|
|||
|
||||
@Override
|
||||
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
|
||||
return areItemsTheSame(oldItemPosition, newItemPosition);
|
||||
Object x = snapshot.get(oldItemPosition);
|
||||
Object y = current.get(newItemPosition);
|
||||
|
||||
if (x instanceof Mission && y instanceof Mission) {
|
||||
return ((Mission) x).storage.equals(((Mission) y).storage);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,11 +6,9 @@ import android.app.NotificationManager;
|
|||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
|
@ -21,12 +19,14 @@ import android.net.NetworkRequest;
|
|||
import android.net.Uri;
|
||||
import android.os.Binder;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v4.app.NotificationCompat.Builder;
|
||||
import android.support.v4.content.PermissionChecker;
|
||||
|
@ -39,9 +39,13 @@ import org.schabi.newpipe.download.DownloadActivity;
|
|||
import org.schabi.newpipe.player.helper.LockManager;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
import us.shandian.giga.io.StoredDirectoryHelper;
|
||||
import us.shandian.giga.io.StoredFileHelper;
|
||||
import us.shandian.giga.postprocessing.Postprocessing;
|
||||
import us.shandian.giga.service.DownloadManager.NetworkState;
|
||||
|
||||
import static org.schabi.newpipe.BuildConfig.APPLICATION_ID;
|
||||
|
@ -61,19 +65,19 @@ public class DownloadManagerService extends Service {
|
|||
private static final int DOWNLOADS_NOTIFICATION_ID = 1001;
|
||||
|
||||
private static final String EXTRA_URLS = "DownloadManagerService.extra.urls";
|
||||
private static final String EXTRA_NAME = "DownloadManagerService.extra.name";
|
||||
private static final String EXTRA_LOCATION = "DownloadManagerService.extra.location";
|
||||
private static final String EXTRA_PATH = "DownloadManagerService.extra.path";
|
||||
private static final String EXTRA_KIND = "DownloadManagerService.extra.kind";
|
||||
private static final String EXTRA_THREADS = "DownloadManagerService.extra.threads";
|
||||
private static final String EXTRA_POSTPROCESSING_NAME = "DownloadManagerService.extra.postprocessingName";
|
||||
private static final String EXTRA_POSTPROCESSING_ARGS = "DownloadManagerService.extra.postprocessingArgs";
|
||||
private static final String EXTRA_SOURCE = "DownloadManagerService.extra.source";
|
||||
private static final String EXTRA_NEAR_LENGTH = "DownloadManagerService.extra.nearLength";
|
||||
private static final String EXTRA_MAIN_STORAGE_TAG = "DownloadManagerService.extra.tag";
|
||||
|
||||
private static final String ACTION_RESET_DOWNLOAD_FINISHED = APPLICATION_ID + ".reset_download_finished";
|
||||
private static final String ACTION_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_finished";
|
||||
|
||||
private DMBinder mBinder;
|
||||
private DownloadManagerBinder mBinder;
|
||||
private DownloadManager mManager;
|
||||
private Notification mNotification;
|
||||
private Handler mHandler;
|
||||
|
@ -110,10 +114,10 @@ public class DownloadManagerService extends Service {
|
|||
/**
|
||||
* notify media scanner on downloaded media file ...
|
||||
*
|
||||
* @param file the downloaded file
|
||||
* @param file the downloaded file uri
|
||||
*/
|
||||
private void notifyMediaScanner(File file) {
|
||||
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file)));
|
||||
private void notifyMediaScanner(Uri file) {
|
||||
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, file));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -124,7 +128,7 @@ public class DownloadManagerService extends Service {
|
|||
Log.d(TAG, "onCreate");
|
||||
}
|
||||
|
||||
mBinder = new DMBinder();
|
||||
mBinder = new DownloadManagerBinder();
|
||||
mHandler = new Handler(Looper.myLooper()) {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
|
@ -186,10 +190,12 @@ public class DownloadManagerService extends Service {
|
|||
handlePreferenceChange(mPrefs, getString(R.string.downloads_queue_limit));
|
||||
|
||||
mLock = new LockManager(this);
|
||||
|
||||
setupStorageAPI(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
public int onStartCommand(final Intent intent, int flags, int startId) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, intent == null ? "Restarting" : "Starting");
|
||||
}
|
||||
|
@ -200,20 +206,7 @@ public class DownloadManagerService extends Service {
|
|||
String action = intent.getAction();
|
||||
if (action != null) {
|
||||
if (action.equals(Intent.ACTION_RUN)) {
|
||||
String[] urls = intent.getStringArrayExtra(EXTRA_URLS);
|
||||
String name = intent.getStringExtra(EXTRA_NAME);
|
||||
String location = intent.getStringExtra(EXTRA_LOCATION);
|
||||
int threads = intent.getIntExtra(EXTRA_THREADS, 1);
|
||||
char kind = intent.getCharExtra(EXTRA_KIND, '?');
|
||||
String psName = intent.getStringExtra(EXTRA_POSTPROCESSING_NAME);
|
||||
String[] psArgs = intent.getStringArrayExtra(EXTRA_POSTPROCESSING_ARGS);
|
||||
String source = intent.getStringExtra(EXTRA_SOURCE);
|
||||
long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0);
|
||||
|
||||
handleConnectivityState(true);// first check the actual network status
|
||||
|
||||
mHandler.post(() -> mManager.startMission(urls, location, name, kind, threads, source, psName, psArgs, nearLength));
|
||||
|
||||
mHandler.post(() -> startMission(intent));
|
||||
} else if (downloadDoneNotification != null) {
|
||||
if (action.equals(ACTION_RESET_DOWNLOAD_FINISHED) || action.equals(ACTION_OPEN_DOWNLOADS_FINISHED)) {
|
||||
downloadDoneCount = 0;
|
||||
|
@ -264,12 +257,12 @@ public class DownloadManagerService extends Service {
|
|||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
int permissionCheck;
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
|
||||
permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE);
|
||||
if (permissionCheck == PermissionChecker.PERMISSION_DENIED) {
|
||||
Toast.makeText(this, "Permission denied (read)", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
// if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
|
||||
// permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE);
|
||||
// if (permissionCheck == PermissionChecker.PERMISSION_DENIED) {
|
||||
// Toast.makeText(this, "Permission denied (read)", Toast.LENGTH_SHORT).show();
|
||||
// }
|
||||
// }
|
||||
|
||||
permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE);
|
||||
if (permissionCheck == PermissionChecker.PERMISSION_DENIED) {
|
||||
|
@ -284,8 +277,8 @@ public class DownloadManagerService extends Service {
|
|||
|
||||
switch (msg.what) {
|
||||
case MESSAGE_FINISHED:
|
||||
notifyMediaScanner(mission.getDownloadedFile());
|
||||
notifyFinishedDownload(mission.name);
|
||||
notifyMediaScanner(mission.storage.getUri());
|
||||
notifyFinishedDownload(mission.storage.getName());
|
||||
mManager.setFinished(mission);
|
||||
handleConnectivityState(false);
|
||||
updateForegroundState(mManager.runMissions());
|
||||
|
@ -344,7 +337,7 @@ public class DownloadManagerService extends Service {
|
|||
if (key.equals(getString(R.string.downloads_maximum_retry))) {
|
||||
try {
|
||||
String value = prefs.getString(key, getString(R.string.downloads_maximum_retry_default));
|
||||
mManager.mPrefMaxRetry = Integer.parseInt(value);
|
||||
mManager.mPrefMaxRetry = value == null ? 0 : Integer.parseInt(value);
|
||||
} catch (Exception e) {
|
||||
mManager.mPrefMaxRetry = 0;
|
||||
}
|
||||
|
@ -353,6 +346,12 @@ public class DownloadManagerService extends Service {
|
|||
mManager.mPrefMeteredDownloads = prefs.getBoolean(key, false);
|
||||
} else if (key.equals(getString(R.string.downloads_queue_limit))) {
|
||||
mManager.mPrefQueueLimit = prefs.getBoolean(key, true);
|
||||
} else if (key.equals(getString(R.string.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;
|
||||
}
|
||||
|
||||
public static void startMission(Context context, String urls[], String location, String name, char kind,
|
||||
/**
|
||||
* Start a new download mission
|
||||
*
|
||||
* @param context the activity context
|
||||
* @param urls the list of urls to download
|
||||
* @param storage where the file is saved
|
||||
* @param kind type of file (a: audio v: video s: subtitle ?: file-extension defined)
|
||||
* @param threads the number of threads maximal used to download chunks of the file.
|
||||
* @param psName the name of the required post-processing algorithm, or {@code null} to ignore.
|
||||
* @param source source url of the resource
|
||||
* @param psArgs the arguments for the post-processing algorithm.
|
||||
* @param nearLength the approximated final length of the file
|
||||
*/
|
||||
public static void startMission(Context context, String urls[], StoredFileHelper storage, char kind,
|
||||
int threads, String source, String psName, String[] psArgs, long nearLength) {
|
||||
Intent intent = new Intent(context, DownloadManagerService.class);
|
||||
intent.setAction(Intent.ACTION_RUN);
|
||||
intent.putExtra(EXTRA_URLS, urls);
|
||||
intent.putExtra(EXTRA_NAME, name);
|
||||
intent.putExtra(EXTRA_LOCATION, location);
|
||||
intent.putExtra(EXTRA_PATH, storage.getUri());
|
||||
intent.putExtra(EXTRA_KIND, kind);
|
||||
intent.putExtra(EXTRA_THREADS, threads);
|
||||
intent.putExtra(EXTRA_SOURCE, source);
|
||||
intent.putExtra(EXTRA_POSTPROCESSING_NAME, psName);
|
||||
intent.putExtra(EXTRA_POSTPROCESSING_ARGS, psArgs);
|
||||
intent.putExtra(EXTRA_NEAR_LENGTH, nearLength);
|
||||
intent.putExtra(EXTRA_MAIN_STORAGE_TAG, storage.getTag());
|
||||
context.startService(intent);
|
||||
}
|
||||
|
||||
public static void checkForRunningMission(Context context, String location, String name, DMChecker checker) {
|
||||
Intent intent = new Intent();
|
||||
intent.setClass(context, DownloadManagerService.class);
|
||||
context.startService(intent);
|
||||
public void startMission(Intent intent) {
|
||||
String[] urls = intent.getStringArrayExtra(EXTRA_URLS);
|
||||
Uri path = intent.getParcelableExtra(EXTRA_PATH);
|
||||
int threads = intent.getIntExtra(EXTRA_THREADS, 1);
|
||||
char kind = intent.getCharExtra(EXTRA_KIND, '?');
|
||||
String psName = intent.getStringExtra(EXTRA_POSTPROCESSING_NAME);
|
||||
String[] psArgs = intent.getStringArrayExtra(EXTRA_POSTPROCESSING_ARGS);
|
||||
String source = intent.getStringExtra(EXTRA_SOURCE);
|
||||
long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0);
|
||||
String tag = intent.getStringExtra(EXTRA_MAIN_STORAGE_TAG);
|
||||
|
||||
context.bindService(intent, new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName cname, IBinder service) {
|
||||
StoredFileHelper storage;
|
||||
try {
|
||||
((DMBinder) service).getDownloadManager().checkForRunningMission(location, name, checker);
|
||||
} catch (Exception err) {
|
||||
Log.w(TAG, "checkForRunningMission() callback is defective", err);
|
||||
storage = new StoredFileHelper(this, path, tag);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);// this never should happen
|
||||
}
|
||||
|
||||
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
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
}
|
||||
}, Context.BIND_AUTO_CREATE);
|
||||
handleConnectivityState(true);// first check the actual network status
|
||||
|
||||
mManager.startMission(mission);
|
||||
}
|
||||
|
||||
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) {
|
||||
downloadFailedNotification.setContentTitle(getString(R.string.app_name));
|
||||
downloadFailedNotification.setStyle(new NotificationCompat.BigTextStyle()
|
||||
.bigText(getString(R.string.download_failed).concat(": ").concat(mission.name)));
|
||||
.bigText(getString(R.string.download_failed).concat(": ").concat(mission.storage.getName())));
|
||||
} else {
|
||||
downloadFailedNotification.setContentTitle(getString(R.string.download_failed));
|
||||
downloadFailedNotification.setContentText(mission.name);
|
||||
downloadFailedNotification.setContentText(mission.storage.getName());
|
||||
downloadFailedNotification.setStyle(new NotificationCompat.BigTextStyle()
|
||||
.bigText(mission.name));
|
||||
.bigText(mission.storage.getName()));
|
||||
}
|
||||
|
||||
mNotificationManager.notify(id, downloadFailedNotification.build());
|
||||
|
@ -508,16 +525,81 @@ public class DownloadManagerService extends Service {
|
|||
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
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public class DMBinder extends Binder {
|
||||
public class DownloadManagerBinder extends Binder {
|
||||
public DownloadManager getDownloadManager() {
|
||||
return mManager;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public StoredDirectoryHelper getMainStorageVideo() {
|
||||
return mManager.mMainStorageVideo;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public StoredDirectoryHelper getMainStorageAudio() {
|
||||
return mManager.mMainStorageAudio;
|
||||
}
|
||||
|
||||
public void addMissionEventListener(Handler handler) {
|
||||
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.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
|
@ -49,6 +48,7 @@ import java.util.Collections;
|
|||
import us.shandian.giga.get.DownloadMission;
|
||||
import us.shandian.giga.get.FinishedMission;
|
||||
import us.shandian.giga.get.Mission;
|
||||
import us.shandian.giga.io.StoredFileHelper;
|
||||
import us.shandian.giga.service.DownloadManager;
|
||||
import us.shandian.giga.service.DownloadManagerService;
|
||||
import us.shandian.giga.ui.common.Deleter;
|
||||
|
@ -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_HOLD;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_STOPPED;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_PROGRESS_LOST;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_SSL_EXCEPTION;
|
||||
import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION;
|
||||
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 mPauseButton;
|
||||
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;
|
||||
mDownloadManager = downloadManager;
|
||||
mDeleter = null;
|
||||
|
@ -156,7 +158,11 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||
|
||||
if (h.item.mission instanceof DownloadMission) {
|
||||
mPendingDownloadsItems.remove(h);
|
||||
if (mPendingDownloadsItems.size() < 1) setAutoRefresh(false);
|
||||
if (mPendingDownloadsItems.size() < 1) {
|
||||
setAutoRefresh(false);
|
||||
if (mStartButton != null) mStartButton.setVisible(false);
|
||||
if (mPauseButton != null) mPauseButton.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
h.popupMenu.dismiss();
|
||||
|
@ -189,10 +195,10 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||
ViewHolderItem h = (ViewHolderItem) view;
|
||||
h.item = item;
|
||||
|
||||
Utility.FileType type = Utility.getFileType(item.mission.kind, item.mission.name);
|
||||
Utility.FileType type = Utility.getFileType(item.mission.kind, item.mission.storage.getName());
|
||||
|
||||
h.icon.setImageResource(Utility.getIconForFileType(type));
|
||||
h.name.setText(item.mission.name);
|
||||
h.name.setText(item.mission.storage.getName());
|
||||
|
||||
h.progress.setColors(Utility.getBackgroundForFileType(mContext, type), Utility.getForegroundForFileType(mContext, type));
|
||||
|
||||
|
@ -273,7 +279,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||
long length = mission.getLength();
|
||||
|
||||
int state;
|
||||
if (mission.isPsFailed()) {
|
||||
if (mission.isPsFailed() || mission.errCode == ERROR_POSTPROCESSING_HOLD) {
|
||||
state = 0;
|
||||
} else if (!mission.running) {
|
||||
state = mission.enqueued ? 1 : 2;
|
||||
|
@ -334,11 +340,17 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||
if (BuildConfig.DEBUG)
|
||||
Log.v(TAG, "Mime: " + mimeType + " package: " + BuildConfig.APPLICATION_ID + ".provider");
|
||||
|
||||
Uri uri = FileProvider.getUriForFile(
|
||||
Uri uri;
|
||||
|
||||
if (mission.storage.isDirect()) {
|
||||
uri = FileProvider.getUriForFile(
|
||||
mContext,
|
||||
BuildConfig.APPLICATION_ID + ".provider",
|
||||
mission.getDownloadedFile()
|
||||
mission.storage.getIOFile()
|
||||
);
|
||||
} else {
|
||||
uri = mission.storage.getUri();
|
||||
}
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
|
@ -366,13 +378,13 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||
|
||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||
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));
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1));
|
||||
|
@ -381,7 +393,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||
}
|
||||
|
||||
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();
|
||||
return true;
|
||||
|
@ -462,6 +474,8 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||
case ERROR_UNKNOWN_EXCEPTION:
|
||||
showError(mission.errObject, UserAction.DOWNLOAD_FAILED, R.string.general_error);
|
||||
return;
|
||||
case ERROR_PROGRESS_LOST:
|
||||
msg = R.string.error_progress_lost;
|
||||
default:
|
||||
if (mission.errCode >= 100 && mission.errCode < 600) {
|
||||
msgEx = "HTTP " + mission.errCode;
|
||||
|
@ -490,7 +504,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||
}
|
||||
|
||||
builder.setNegativeButton(android.R.string.ok, (dialog, which) -> dialog.cancel())
|
||||
.setTitle(mission.name)
|
||||
.setTitle(mission.storage.getName())
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
@ -539,6 +553,10 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||
updateProgress(h);
|
||||
return true;
|
||||
case R.id.retry:
|
||||
if (mission.hasInvalidStorage()) {
|
||||
mRecover.tryRecover(mission);
|
||||
return true;
|
||||
}
|
||||
mission.psContinue(true);
|
||||
return true;
|
||||
case R.id.cancel:
|
||||
|
@ -561,7 +579,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||
return true;
|
||||
case R.id.md5:
|
||||
case R.id.sha1:
|
||||
new ChecksumTask(mContext).execute(h.item.mission.getDownloadedFile().getAbsolutePath(), ALGORITHMS.get(id));
|
||||
new ChecksumTask(mContext).execute(h.item.mission.storage, ALGORITHMS.get(id));
|
||||
return true;
|
||||
case R.id.source:
|
||||
/*Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(h.item.mission.source));
|
||||
|
@ -641,19 +659,38 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||
}
|
||||
|
||||
|
||||
public void deleterDispose(Bundle bundle) {
|
||||
if (mDeleter != null) mDeleter.dispose(bundle);
|
||||
public void deleterDispose(boolean commitChanges) {
|
||||
if (mDeleter != null) mDeleter.dispose(commitChanges);
|
||||
}
|
||||
|
||||
public void deleterLoad(Bundle bundle, View view) {
|
||||
public void deleterLoad(View view) {
|
||||
if (mDeleter == null)
|
||||
mDeleter = new Deleter(bundle, view, mContext, this, mDownloadManager, mIterator, mHandler);
|
||||
mDeleter = new Deleter(view, mContext, this, mDownloadManager, mIterator, mHandler);
|
||||
}
|
||||
|
||||
public void deleterResume() {
|
||||
if (mDeleter != null) mDeleter.resume();
|
||||
}
|
||||
|
||||
public void recoverMission(DownloadMission mission, 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 final Runnable rUpdater = this::updater;
|
||||
|
@ -695,6 +732,10 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||
return Float.isNaN(value) || Float.isInfinite(value);
|
||||
}
|
||||
|
||||
public void setRecover(@NonNull RecoverHelper callback) {
|
||||
mRecover = callback;
|
||||
}
|
||||
|
||||
|
||||
class ViewHolderItem extends RecyclerView.ViewHolder {
|
||||
DownloadManager.MissionItem item;
|
||||
|
@ -780,7 +821,11 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||
DownloadMission mission = item.mission instanceof DownloadMission ? (DownloadMission) item.mission : null;
|
||||
|
||||
if (mission != null) {
|
||||
if (mission.isPsRunning()) {
|
||||
if (mission.hasInvalidStorage()) {
|
||||
retry.setEnabled(true);
|
||||
delete.setEnabled(true);
|
||||
showError.setEnabled(true);
|
||||
} else if (mission.isPsRunning()) {
|
||||
switch (mission.errCode) {
|
||||
case ERROR_INSUFFICIENT_STORAGE:
|
||||
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;
|
||||
WeakReference<Activity> weakReference;
|
||||
|
||||
|
@ -861,8 +906,8 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected String doInBackground(String... params) {
|
||||
return Utility.checksum(params[0], params[1]);
|
||||
protected String doInBackground(Object... params) {
|
||||
return Utility.checksum((StoredFileHelper) params[0], (String) params[1]);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -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.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.view.View;
|
||||
|
@ -23,8 +21,6 @@ public class Deleter {
|
|||
private static final int TIMEOUT = 5000;// ms
|
||||
private static final int DELAY = 350;// ms
|
||||
private static final int DELAY_RESUME = 400;// ms
|
||||
private static final String BUNDLE_NAMES = "us.shandian.giga.ui.common.deleter.names";
|
||||
private static final String BUNDLE_LOCATIONS = "us.shandian.giga.ui.common.deleter.locations";
|
||||
|
||||
private Snackbar snackbar;
|
||||
private ArrayList<Mission> items;
|
||||
|
@ -41,7 +37,7 @@ public class Deleter {
|
|||
private final Runnable rNext;
|
||||
private final Runnable rCommit;
|
||||
|
||||
public Deleter(Bundle b, View v, Context c, MissionAdapter a, DownloadManager d, MissionIterator i, Handler h) {
|
||||
public Deleter(View v, Context c, MissionAdapter a, DownloadManager d, MissionIterator i, Handler h) {
|
||||
mView = v;
|
||||
mContext = c;
|
||||
mAdapter = a;
|
||||
|
@ -55,27 +51,6 @@ public class Deleter {
|
|||
rCommit = this::commit;
|
||||
|
||||
items = new ArrayList<>(2);
|
||||
|
||||
if (b != null) {
|
||||
String[] names = b.getStringArray(BUNDLE_NAMES);
|
||||
String[] locations = b.getStringArray(BUNDLE_LOCATIONS);
|
||||
|
||||
if (names == null || locations == null) return;
|
||||
if (names.length < 1 || locations.length < 1) return;
|
||||
if (names.length != locations.length) return;
|
||||
|
||||
items.ensureCapacity(names.length);
|
||||
|
||||
for (int j = 0; j < locations.length; j++) {
|
||||
Mission mission = mDownloadManager.getAnyMission(locations[j], names[j]);
|
||||
if (mission == null) continue;
|
||||
|
||||
items.add(mission);
|
||||
mIterator.hide(mission);
|
||||
}
|
||||
|
||||
if (items.size() > 0) resume();
|
||||
}
|
||||
}
|
||||
|
||||
public void append(Mission item) {
|
||||
|
@ -104,7 +79,7 @@ public class Deleter {
|
|||
private void next() {
|
||||
if (items.size() < 1) return;
|
||||
|
||||
String msg = mContext.getString(R.string.file_deleted).concat(":\n").concat(items.get(0).name);
|
||||
String msg = mContext.getString(R.string.file_deleted).concat(":\n").concat(items.get(0).storage.getName());
|
||||
|
||||
snackbar = Snackbar.make(mView, msg, Snackbar.LENGTH_INDEFINITE);
|
||||
snackbar.setAction(R.string.undo, s -> forget());
|
||||
|
@ -125,7 +100,7 @@ public class Deleter {
|
|||
mDownloadManager.deleteMission(mission);
|
||||
|
||||
if (mission instanceof FinishedMission) {
|
||||
mContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(mission.getDownloadedFile())));
|
||||
mContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, mission.storage.getUri()));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -151,27 +126,14 @@ public class Deleter {
|
|||
mHandler.postDelayed(rShow, DELAY_RESUME);
|
||||
}
|
||||
|
||||
public void dispose(Bundle bundle) {
|
||||
public void dispose(boolean commitChanges) {
|
||||
if (items.size() < 1) return;
|
||||
|
||||
pause();
|
||||
|
||||
if (bundle == null) {
|
||||
if (!commitChanges) return;
|
||||
|
||||
for (Mission mission : items) mDownloadManager.deleteMission(mission);
|
||||
items = null;
|
||||
return;
|
||||
}
|
||||
|
||||
String[] names = new String[items.size()];
|
||||
String[] locations = new String[items.size()];
|
||||
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
Mission mission = items.get(i);
|
||||
names[i] = mission.name;
|
||||
locations[i] = mission.location;
|
||||
}
|
||||
|
||||
bundle.putStringArray(BUNDLE_NAMES, names);
|
||||
bundle.putStringArray(BUNDLE_LOCATIONS, locations);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package us.shandian.giga.ui.fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Fragment;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
@ -10,6 +9,7 @@ import android.content.SharedPreferences;
|
|||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v7.widget.GridLayoutManager;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
|
@ -18,18 +18,24 @@ import android.view.Menu;
|
|||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
import us.shandian.giga.io.StoredFileHelper;
|
||||
import us.shandian.giga.service.DownloadManager;
|
||||
import us.shandian.giga.service.DownloadManagerService;
|
||||
import us.shandian.giga.service.DownloadManagerService.DMBinder;
|
||||
import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder;
|
||||
import us.shandian.giga.ui.adapter.MissionAdapter;
|
||||
|
||||
public class MissionsFragment extends Fragment {
|
||||
|
||||
private static final int SPAN_SIZE = 2;
|
||||
private static final int REQUEST_DOWNLOAD_PATH_SAF = 0x1230;
|
||||
|
||||
private SharedPreferences mPrefs;
|
||||
private boolean mLinear;
|
||||
|
@ -45,24 +51,32 @@ public class MissionsFragment extends Fragment {
|
|||
private LinearLayoutManager mLinearManager;
|
||||
private Context mContext;
|
||||
|
||||
private DMBinder mBinder;
|
||||
private Bundle mBundle;
|
||||
private DownloadManagerBinder mBinder;
|
||||
private boolean mForceUpdate;
|
||||
|
||||
private DownloadMission unsafeMissionTarget = null;
|
||||
|
||||
private ServiceConnection mConnection = new ServiceConnection() {
|
||||
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder binder) {
|
||||
mBinder = (DownloadManagerService.DMBinder) binder;
|
||||
mBinder = (DownloadManagerBinder) binder;
|
||||
mBinder.clearDownloadNotifications();
|
||||
|
||||
mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), 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();
|
||||
|
||||
mBundle = null;
|
||||
|
||||
mBinder.addMissionEventListener(mAdapter.getMessenger());
|
||||
mBinder.enableNotifications(false);
|
||||
|
||||
|
@ -84,9 +98,6 @@ public class MissionsFragment extends Fragment {
|
|||
mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
||||
mLinear = mPrefs.getBoolean("linear", false);
|
||||
|
||||
//mContext = getActivity().getApplicationContext();
|
||||
mBundle = savedInstanceState;
|
||||
|
||||
// Bind the service
|
||||
mContext.bindService(new Intent(mContext, DownloadManagerService.class), mConnection, Context.BIND_AUTO_CREATE);
|
||||
|
||||
|
@ -148,7 +159,7 @@ public class MissionsFragment extends Fragment {
|
|||
mBinder.removeMissionEventListener(mAdapter.getMessenger());
|
||||
mBinder.enableNotifications(true);
|
||||
mContext.unbindService(mConnection);
|
||||
mAdapter.deleterDispose(null);
|
||||
mAdapter.deleterDispose(true);
|
||||
|
||||
mBinder = null;
|
||||
mAdapter = null;
|
||||
|
@ -178,10 +189,12 @@ public class MissionsFragment extends Fragment {
|
|||
return true;
|
||||
case R.id.start_downloads:
|
||||
item.setVisible(false);
|
||||
mPause.setVisible(true);
|
||||
mBinder.getDownloadManager().startAllMissions();
|
||||
return true;
|
||||
case R.id.pause_downloads:
|
||||
item.setVisible(false);
|
||||
mStart.setVisible(true);
|
||||
mBinder.getDownloadManager().pauseAllMissions(false);
|
||||
mAdapter.ensurePausedMissions();// update items view
|
||||
default:
|
||||
|
@ -231,7 +244,7 @@ public class MissionsFragment extends Fragment {
|
|||
super.onSaveInstanceState(outState);
|
||||
|
||||
if (mAdapter != null) {
|
||||
mAdapter.deleterDispose(outState);
|
||||
mAdapter.deleterDispose(false);
|
||||
mForceUpdate = true;
|
||||
mBinder.removeMissionEventListener(mAdapter.getMessenger());
|
||||
}
|
||||
|
@ -260,4 +273,22 @@ public class MissionsFragment extends Fragment {
|
|||
if (mAdapter != null) mAdapter.onPaused();
|
||||
if (mBinder != null) mBinder.enableNotifications(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (requestCode != REQUEST_DOWNLOAD_PATH_SAF || resultCode != Activity.RESULT_OK) return;
|
||||
|
||||
if (unsafeMissionTarget == null || data.getData() == null) {
|
||||
return;// unsafeMissionTarget cannot be null
|
||||
}
|
||||
|
||||
try {
|
||||
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 org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
|
@ -25,7 +25,8 @@ import java.io.Serializable;
|
|||
import java.net.HttpURLConnection;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Locale;
|
||||
|
||||
import us.shandian.giga.io.StoredFileHelper;
|
||||
|
||||
public class Utility {
|
||||
|
||||
|
@ -206,7 +207,7 @@ public class Utility {
|
|||
Toast.makeText(context, R.string.msg_copied, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
public static String checksum(String path, String algorithm) {
|
||||
public static String checksum(StoredFileHelper source, String algorithm) {
|
||||
MessageDigest md;
|
||||
|
||||
try {
|
||||
|
@ -215,11 +216,11 @@ public class Utility {
|
|||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
FileInputStream i;
|
||||
SharpStream i;
|
||||
|
||||
try {
|
||||
i = new FileInputStream(path);
|
||||
} catch (FileNotFoundException e) {
|
||||
i = source.getStream();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
|
@ -247,15 +248,15 @@ public class Utility {
|
|||
}
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
public static boolean mkdir(File path, boolean allDirs) {
|
||||
if (path.exists()) return true;
|
||||
public static boolean mkdir(File p, boolean allDirs) {
|
||||
if (p.exists()) return true;
|
||||
|
||||
if (allDirs)
|
||||
path.mkdirs();
|
||||
p.mkdirs();
|
||||
else
|
||||
path.mkdir();
|
||||
p.mkdir();
|
||||
|
||||
return path.exists();
|
||||
return p.exists();
|
||||
}
|
||||
|
||||
public static long getContentLength(HttpURLConnection connection) {
|
||||
|
|
|
@ -462,12 +462,12 @@
|
|||
<string name="download_finished_more">%s أنتهى التحميل</string>
|
||||
<string name="generate_unique_name">إنشاء اسم فريد</string>
|
||||
<string name="overwrite">الكتابة فوق</string>
|
||||
<string name="overwrite_warning">يوجد ملف تحميل بهذا الاسم موجود مسبقاً</string>
|
||||
<string name="overwrite_finished_warning">يوجد ملف تحميل بهذا الاسم موجود مسبقاً</string>
|
||||
<string name="download_already_running">هنالك تحميل قيد التقدم بهذا الاسم</string>
|
||||
<string name="show_error">إظهار خطأ</string>
|
||||
<string name="label_code">كود</string>
|
||||
<string name="error_path_creation">لا يمكن إنشاء الملف</string>
|
||||
<string name="error_file_creation">لا يمكن إنشاء المجلد الوجهة</string>
|
||||
<string name="error_file_creation">لا يمكن إنشاء الملف</string>
|
||||
<string name="error_path_creation">لا يمكن إنشاء المجلد الوجهة</string>
|
||||
<string name="error_permission_denied">تم رفضها من قبل النظام</string>
|
||||
<string name="error_ssl_exception">فشل اتصال الأمن</string>
|
||||
<string name="error_unknown_host">تعذر العثور على الخادم</string>
|
||||
|
|
|
@ -432,8 +432,8 @@
|
|||
<string name="generate_unique_name">Genera un nom únic</string>
|
||||
<string name="show_error">Mostra l\'error</string>
|
||||
<string name="label_code">Codi</string>
|
||||
<string name="error_path_creation">No es pot crear el fitxer</string>
|
||||
<string name="error_file_creation">No es pot crear la carpeta de destinació</string>
|
||||
<string name="error_file_creation">No es pot crear el fitxer</string>
|
||||
<string name="error_path_creation">No es pot crear la carpeta de destinació</string>
|
||||
<string name="stop">Atura</string>
|
||||
<string name="events">Esdeveniments</string>
|
||||
<string name="app_update_notification_channel_description">Notificacions de noves versions del NewPipe</string>
|
||||
|
|
|
@ -437,11 +437,11 @@
|
|||
<string name="download_finished_more">%s已下载完毕</string>
|
||||
<string name="generate_unique_name">生成独特的名字</string>
|
||||
<string name="overwrite">覆写</string>
|
||||
<string name="overwrite_warning">同名的已下载文件已经存在</string>
|
||||
<string name="overwrite_finished_warning">同名的已下载文件已经存在</string>
|
||||
<string name="download_already_running">同名下载进行中</string>
|
||||
<string name="show_error">显示错误</string>
|
||||
<string name="label_code">代码</string>
|
||||
<string name="error_path_creation">无法创建该文件</string>
|
||||
<string name="error_file_creation">无法创建该文件</string>
|
||||
<string name="error_permission_denied">系统拒绝此批准</string>
|
||||
<string name="error_ssl_exception">安全连接失败</string>
|
||||
<string name="error_unknown_host">找不到服务器</string>
|
||||
|
@ -464,7 +464,7 @@
|
|||
<string name="grid">网格</string>
|
||||
<string name="switch_view">切换视图</string>
|
||||
<string name="app_update_notification_content_title">NewPipe 更新可用!</string>
|
||||
<string name="error_file_creation">无法创建目标文件夹</string>
|
||||
<string name="error_path_creation">无法创建目标文件夹</string>
|
||||
<string name="error_http_unsupported_range">服务器不接受多线程下载, 请重试使用 @string/msg_threads = 1</string>
|
||||
<string name="error_http_requested_range_not_satisfiable">请求范围无法满足</string>
|
||||
<string name="msg_pending_downloads">继续进行%s个待下载转移</string>
|
||||
|
|
|
@ -372,8 +372,8 @@
|
|||
<string name="download_already_running">Der er en download i gang med dette navn</string>
|
||||
<string name="show_error">Vis fejl</string>
|
||||
<string name="label_code">Kode</string>
|
||||
<string name="error_path_creation">Filen kan ikke oprettes</string>
|
||||
<string name="error_file_creation">Destinationsmappen kan ikke oprettes</string>
|
||||
<string name="error_file_creation">Filen kan ikke oprettes</string>
|
||||
<string name="error_path_creation">Destinationsmappen kan ikke oprettes</string>
|
||||
<string name="error_permission_denied">Adgang nægtet af systemet</string>
|
||||
<string name="error_ssl_exception">Sikker forbindelse fejlede</string>
|
||||
<string name="error_unknown_host">Kunne ikke finde serveren</string>
|
||||
|
|
|
@ -448,12 +448,12 @@
|
|||
<string name="download_finished_more">%s heruntergeladen</string>
|
||||
<string name="generate_unique_name">Eindeutigen Namen erzeugen</string>
|
||||
<string name="overwrite">Überschreiben</string>
|
||||
<string name="overwrite_warning">Eine heruntergeladene Datei dieses Namens existiert bereits</string>
|
||||
<string name="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="show_error">Fehler anzeigen</string>
|
||||
<string name="label_code">Code</string>
|
||||
<string name="error_path_creation">Die Datei kann nicht erstellt werden</string>
|
||||
<string name="error_file_creation">Der Zielordner kann nicht erstellt werden</string>
|
||||
<string name="error_file_creation">Die Datei kann nicht erstellt werden</string>
|
||||
<string name="error_path_creation">Der Zielordner kann nicht erstellt werden</string>
|
||||
<string name="error_permission_denied">System verweigert den Zugriff</string>
|
||||
<string name="error_ssl_exception">Sichere Verbindung fehlgeschlagen</string>
|
||||
<string name="error_unknown_host">Der Server konnte nicht gefunden werden</string>
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<string name="share_dialog_title">Compartir con</string>
|
||||
<string name="choose_browser">Elegir navegador</string>
|
||||
<string name="screen_rotation">rotación</string>
|
||||
<string name="download_path_title">Ruta de descarga de vídeo</string>
|
||||
<string name="download_path_title">Carpeta de descarga de vídeo</string>
|
||||
<string name="download_path_summary">Ruta para almacenar los vídeos descargados</string>
|
||||
<string name="download_path_dialog_title">Introducir directorio de descargas para vídeos</string>
|
||||
<string name="default_resolution_title">Resolución por defecto de vídeo</string>
|
||||
|
@ -40,7 +40,7 @@
|
|||
<string name="use_tor_summary">(Experimental) Forzar la descarga a través de Tor para una mayor privacidad (transmisión de vídeos aún no compatible).</string>
|
||||
<string name="err_dir_create">No se puede crear la carpeta de descarga \'%1$s\'</string>
|
||||
<string name="info_dir_created">Carpeta de descarga creada \'%1$s\'</string>
|
||||
<string name="download_path_audio_summary">Los audios descargados se almacenan aquí</string>
|
||||
<string name="download_path_audio_summary">Ruta para almacenar los audios descargados</string>
|
||||
<string name="download_path_audio_dialog_title">Introducir ruta de descarga para archivos de audio</string>
|
||||
<string name="blocked_by_gema">Bloqueado por GEMA</string>
|
||||
<string name="download_path_audio_title">Carpeta de descarga de audio</string>
|
||||
|
@ -418,7 +418,9 @@ abrir en modo popup</string>
|
|||
<!-- dialog about existing downloads -->
|
||||
<string name="generate_unique_name">Generar nombre único</string>
|
||||
<string name="overwrite">Sobrescribir</string>
|
||||
<string name="overwrite_warning">Ya existe un archivo descargado con este nombre</string>
|
||||
<string name="overwrite_unrelated_warning">Ya existe un archivo con este nombre</string>
|
||||
<string name="overwrite_finished_warning">Ya existe un archivo descargado con este nombre</string>
|
||||
<string name="overwrite_failed">No se puede sobrescribir el archivo</string>
|
||||
<string name="download_already_running">Hay una descarga en curso con este nombre</string>
|
||||
<string name="download_already_pending">Hay una descarga pendiente con este nombre</string>
|
||||
|
||||
|
@ -440,8 +442,8 @@ abrir en modo popup</string>
|
|||
<!-- message dialog about download error -->
|
||||
<string name="show_error">Mostrar error</string>
|
||||
<string name="label_code">Codigo</string>
|
||||
<string name="error_path_creation">No se puede crear la carpeta de destino</string>
|
||||
<string name="error_file_creation">No se puede crear el archivo</string>
|
||||
<string name="error_file_creation">No se puede crear la carpeta de destino</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_ssl_exception">Fallo la conexión segura</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_stopped">NewPipe se cerro mientras se trabajaba en el archivo</string>
|
||||
<string name="error_insufficient_storage">No hay suficiente espacio disponible en el dispositivo</string>
|
||||
<string name="error_progress_lost">Se perdió el progreso porque el archivo fue eliminado</string>
|
||||
|
||||
<string name="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="tab_new">Nueva pestaña</string>
|
||||
|
|
|
@ -446,12 +446,12 @@
|
|||
<string name="download_finished_more">%s deskarga amaituta</string>
|
||||
<string name="generate_unique_name">Sortu izen bakana</string>
|
||||
<string name="overwrite">Gainidatzi</string>
|
||||
<string name="overwrite_warning">Badago izen bera duen deskargatutako fitxategi bat</string>
|
||||
<string name="overwrite_finished_warning">Badago izen bera duen deskargatutako fitxategi bat</string>
|
||||
<string name="download_already_running">Badago izen bera duen deskarga bat abian</string>
|
||||
<string name="show_error">Erakutsi errorea</string>
|
||||
<string name="label_code">Kodea</string>
|
||||
<string name="error_path_creation">Ezin da fitxategia sortu</string>
|
||||
<string name="error_file_creation">Ezin da helburu karpeta sortu</string>
|
||||
<string name="error_file_creation">Ezin da fitxategia sortu</string>
|
||||
<string name="error_path_creation">Ezin da helburu karpeta sortu</string>
|
||||
<string name="error_permission_denied">Sistemak baimena ukatu du</string>
|
||||
<string name="error_ssl_exception">Konexio seguruak huts egin du</string>
|
||||
<string name="error_unknown_host">Ezin izan da zerbitzaria aurkitu</string>
|
||||
|
|
|
@ -451,12 +451,12 @@
|
|||
<string name="download_finished_more">%s הורדות הסתיימו</string>
|
||||
<string name="generate_unique_name">יצירת שם ייחודי</string>
|
||||
<string name="overwrite">שכתוב</string>
|
||||
<string name="overwrite_warning">כבר קיים קובץ בשם הזה</string>
|
||||
<string name="overwrite_finished_warning">כבר קיים קובץ בשם הזה</string>
|
||||
<string name="download_already_running">אחת ההורדות הפעילות כבר נושאת את השם הזה</string>
|
||||
<string name="show_error">הצגת שגיאה</string>
|
||||
<string name="label_code">קוד</string>
|
||||
<string name="error_path_creation">לא ניתן ליצור את הקובץ</string>
|
||||
<string name="error_file_creation">לא ניתן ליצור את תיקיית היעד</string>
|
||||
<string name="error_file_creation">לא ניתן ליצור את הקובץ</string>
|
||||
<string name="error_path_creation">לא ניתן ליצור את תיקיית היעד</string>
|
||||
<string name="error_permission_denied">ההרשאה נדחתה על ידי המערכת</string>
|
||||
<string name="error_ssl_exception">החיבור המאובטח נכשל</string>
|
||||
<string name="error_unknown_host">לא ניתן למצוא את השרת</string>
|
||||
|
|
|
@ -447,12 +447,12 @@
|
|||
<string name="download_finished_more">%s unduhan selesai</string>
|
||||
<string name="generate_unique_name">Hasilkan nama unik</string>
|
||||
<string name="overwrite">Timpa</string>
|
||||
<string name="overwrite_warning">File yang diunduh dengan nama ini sudah ada</string>
|
||||
<string name="overwrite_finished_warning">File yang diunduh dengan nama ini sudah ada</string>
|
||||
<string name="download_already_running">Ada unduhan yang sedang berlangsung dengan nama ini</string>
|
||||
<string name="show_error">Tunjukkan kesalahan</string>
|
||||
<string name="label_code">Kode</string>
|
||||
<string name="error_path_creation">File tidak dapat dibuat</string>
|
||||
<string name="error_file_creation">Folder tujuan tidak dapat dibuat</string>
|
||||
<string name="error_file_creation">File tidak dapat dibuat</string>
|
||||
<string name="error_path_creation">Folder tujuan tidak dapat dibuat</string>
|
||||
<string name="error_permission_denied">Izin ditolak oleh sistem</string>
|
||||
<string name="error_ssl_exception">Koneksi aman gagal</string>
|
||||
<string name="error_unknown_host">Tidak dapat menemukan server</string>
|
||||
|
|
|
@ -449,12 +449,12 @@
|
|||
<string name="download_finished_more">%s download finiti</string>
|
||||
<string name="generate_unique_name">Genera un nome unico</string>
|
||||
<string name="overwrite">Sovrascrivi</string>
|
||||
<string name="overwrite_warning">Esiste già un file scaricato con lo stesso nome</string>
|
||||
<string name="overwrite_finished_warning">Esiste già un file scaricato con lo stesso nome</string>
|
||||
<string name="download_already_running">C\'è un download in progresso con questo nome</string>
|
||||
<string name="show_error">Mostra errore</string>
|
||||
<string name="label_code">Codice</string>
|
||||
<string name="error_path_creation">Impossibile creare il file</string>
|
||||
<string name="error_file_creation">Impossibile creare la cartella di destinazione</string>
|
||||
<string name="error_file_creation">Impossibile creare il file</string>
|
||||
<string name="error_path_creation">Impossibile creare la cartella di destinazione</string>
|
||||
<string name="error_permission_denied">Permesso negato dal sistema</string>
|
||||
<string name="error_ssl_exception">Connessione sicura fallita</string>
|
||||
<string name="error_unknown_host">Impossibile trovare il server</string>
|
||||
|
|
|
@ -443,12 +443,12 @@
|
|||
<string name="download_finished_more">%s muat turun selesai</string>
|
||||
<string name="generate_unique_name">Menjana nama yang unik</string>
|
||||
<string name="overwrite">Timpa</string>
|
||||
<string name="overwrite_warning">Fail yang dimuat turun dengan nama ini sudah wujud</string>
|
||||
<string name="overwrite_finished_warning">Fail yang dimuat turun dengan nama ini sudah wujud</string>
|
||||
<string name="download_already_running">Terdapat muat turun yang sedang berjalan dengan nama ini</string>
|
||||
<string name="show_error">Tunjukkan kesilapan</string>
|
||||
<string name="label_code">Kod</string>
|
||||
<string name="error_path_creation">Fail tidak boleh dibuat</string>
|
||||
<string name="error_file_creation">Folder destinasi tidak boleh dibuat</string>
|
||||
<string name="error_file_creation">Fail tidak boleh dibuat</string>
|
||||
<string name="error_path_creation">Folder destinasi tidak boleh dibuat</string>
|
||||
<string name="error_permission_denied">Kebenaran ditolak oleh sistem</string>
|
||||
<string name="error_ssl_exception">Sambungan selamat gagal</string>
|
||||
<string name="error_unknown_host">Tidak dapat mencari server</string>
|
||||
|
|
|
@ -526,12 +526,12 @@
|
|||
<string name="download_finished_more">%s nedlastinger fullført</string>
|
||||
<string name="generate_unique_name">Generer unikt navn</string>
|
||||
<string name="overwrite">Overskriv</string>
|
||||
<string name="overwrite_warning">Nedlastet fil ved dette navnet finnes allerede</string>
|
||||
<string name="overwrite_finished_warning">Nedlastet fil ved dette navnet finnes allerede</string>
|
||||
<string name="download_already_running">Nedlasting med dette navnet underveis allerede</string>
|
||||
<string name="show_error">Vis feil</string>
|
||||
<string name="label_code">Kode</string>
|
||||
<string name="error_path_creation">Filen kan ikke opprettes</string>
|
||||
<string name="error_file_creation">Målmappen kan ikke opprettes</string>
|
||||
<string name="error_file_creation">Filen kan ikke opprettes</string>
|
||||
<string name="error_path_creation">Målmappen kan ikke opprettes</string>
|
||||
<string name="error_permission_denied">Tilgang nektet av systemet</string>
|
||||
<string name="error_ssl_exception">Sikker tilkobling mislyktes</string>
|
||||
<string name="error_unknown_host">Fant ikke tjeneren</string>
|
||||
|
|
|
@ -445,12 +445,12 @@
|
|||
<string name="download_finished_more">%s downloads voltooid</string>
|
||||
<string name="generate_unique_name">Unieke naam genereren</string>
|
||||
<string name="overwrite">Overschrijven</string>
|
||||
<string name="overwrite_warning">Der bestaat al een gedownload bestand met deze naam</string>
|
||||
<string name="overwrite_finished_warning">Der bestaat al een gedownload bestand met deze naam</string>
|
||||
<string name="download_already_running">Der is al een download met deze naam bezig</string>
|
||||
<string name="show_error">Foutmelding weergeven</string>
|
||||
<string name="label_code">Code</string>
|
||||
<string name="error_path_creation">Het bestand kan niet aangemaakt worden</string>
|
||||
<string name="error_file_creation">De doelmap kan niet aangemaakt worden</string>
|
||||
<string name="error_file_creation">Het bestand kan niet aangemaakt worden</string>
|
||||
<string name="error_path_creation">De doelmap kan niet aangemaakt worden</string>
|
||||
<string name="error_permission_denied">Toelating geweigerd door het systeem</string>
|
||||
<string name="error_ssl_exception">Beveiligde verbinding is mislukt</string>
|
||||
<string name="error_unknown_host">Kon de server niet vinden</string>
|
||||
|
|
|
@ -449,12 +449,12 @@
|
|||
<string name="download_finished_more">%s downloads voltooid</string>
|
||||
<string name="generate_unique_name">Genereer een unieke naam</string>
|
||||
<string name="overwrite">Overschrijven</string>
|
||||
<string name="overwrite_warning">Er bestaat al een gedownload bestand met deze naam</string>
|
||||
<string name="overwrite_finished_warning">Er bestaat al een gedownload bestand met deze naam</string>
|
||||
<string name="download_already_running">Er is een download aan de gang met deze naam</string>
|
||||
<string name="show_error">Toon foutmelding</string>
|
||||
<string name="label_code">Code</string>
|
||||
<string name="error_path_creation">Het bestand kan niet worden gemaakt</string>
|
||||
<string name="error_file_creation">De doelmap kan niet worden gemaakt</string>
|
||||
<string name="error_file_creation">Het bestand kan niet worden gemaakt</string>
|
||||
<string name="error_path_creation">De doelmap kan niet worden gemaakt</string>
|
||||
<string name="error_permission_denied">Toestemming door het systeem geweigerd</string>
|
||||
<string name="error_ssl_exception">Beveiligde connectie is mislukt</string>
|
||||
<string name="error_unknown_host">Kon de server niet vinden</string>
|
||||
|
|
|
@ -446,12 +446,12 @@
|
|||
<string name="download_finished_more">%s pobieranie zostało zakończone</string>
|
||||
<string name="generate_unique_name">Wygeneruj unikalną nazwę</string>
|
||||
<string name="overwrite">Zastąp</string>
|
||||
<string name="overwrite_warning">Pobrany plik o tej nazwie już istnieje</string>
|
||||
<string name="overwrite_finished_warning">Pobrany plik o tej nazwie już istnieje</string>
|
||||
<string name="download_already_running">Trwa pobieranie z tą nazwą</string>
|
||||
<string name="show_error">Pokaż błąd</string>
|
||||
<string name="label_code">Kod</string>
|
||||
<string name="error_path_creation">Nie można utworzyć pliku</string>
|
||||
<string name="error_file_creation">Nie można utworzyć folderu docelowego</string>
|
||||
<string name="error_file_creation">Nie można utworzyć pliku</string>
|
||||
<string name="error_path_creation">Nie można utworzyć folderu docelowego</string>
|
||||
<string name="error_permission_denied">Odmowa dostępu do systemu</string>
|
||||
<string name="error_ssl_exception">Bezpieczne połączenie nie powiodło się</string>
|
||||
<string name="error_unknown_host">Nie można znaleźć serwera</string>
|
||||
|
|
|
@ -446,12 +446,12 @@ abrir em modo popup</string>
|
|||
<string name="download_finished_more">%s downloads terminados</string>
|
||||
<string name="generate_unique_name">Gerar nome único</string>
|
||||
<string name="overwrite">"Sobrescrever "</string>
|
||||
<string name="overwrite_warning">Um arquivo baixado com esse nome já existe</string>
|
||||
<string name="overwrite_finished_warning">Um arquivo baixado com esse nome já existe</string>
|
||||
<string name="download_already_running">Existe um download em progresso com esse nome</string>
|
||||
<string name="show_error">Mostrar erro</string>
|
||||
<string name="label_code">Código</string>
|
||||
<string name="error_path_creation">O arquivo não pode ser criado</string>
|
||||
<string name="error_file_creation">A pasta de destino não pode ser criada</string>
|
||||
<string name="error_file_creation">O arquivo não pode ser criado</string>
|
||||
<string name="error_path_creation">A pasta de destino não pode ser criada</string>
|
||||
<string name="error_permission_denied">Permissão negada pelo sistema</string>
|
||||
<string name="error_ssl_exception">"Falha na conexão segura "</string>
|
||||
<string name="error_unknown_host">Não foi possível encontrar o servidor</string>
|
||||
|
|
|
@ -442,12 +442,12 @@
|
|||
<string name="download_finished_more">%s descargas terminadas</string>
|
||||
<string name="generate_unique_name">Gerar nome único</string>
|
||||
<string name="overwrite">Sobrescrever</string>
|
||||
<string name="overwrite_warning">Um ficheiro descarregado com este nome já existe</string>
|
||||
<string name="overwrite_finished_warning">Um ficheiro descarregado com este nome já existe</string>
|
||||
<string name="download_already_running">Já existe uma descarga em curso com este nome</string>
|
||||
<string name="show_error">Mostrar erro</string>
|
||||
<string name="label_code">Código</string>
|
||||
<string name="error_path_creation">O ficheiro não pode ser criado</string>
|
||||
<string name="error_file_creation">A pasta de destino não pode ser criada</string>
|
||||
<string name="error_file_creation">O ficheiro não pode ser criado</string>
|
||||
<string name="error_path_creation">A pasta de destino não pode ser criada</string>
|
||||
<string name="error_permission_denied">Permissão negada pelo sistema</string>
|
||||
<string name="error_ssl_exception">Ligação segura falhou</string>
|
||||
<string name="error_unknown_host">Não foi possível encontrar o servidor</string>
|
||||
|
|
|
@ -442,12 +442,12 @@
|
|||
<string name="permission_denied">Действие запрещено системой</string>
|
||||
<string name="download_failed">Ошибка загрузки</string>
|
||||
<string name="overwrite">Перезаписать</string>
|
||||
<string name="overwrite_warning">Файл с таким именем уже существует</string>
|
||||
<string name="overwrite_finished_warning">Файл с таким именем уже существует</string>
|
||||
<string name="download_already_running">Загрузка с таким именем уже выполняется</string>
|
||||
<string name="show_error">Показать текст ошибки</string>
|
||||
<string name="label_code">Код</string>
|
||||
<string name="error_path_creation">Файл не может быть создан</string>
|
||||
<string name="error_file_creation">Папка назначения не может быть создана</string>
|
||||
<string name="error_path_creation">Папка назначения не может быть создана</string>
|
||||
<string name="error_file_creation">Файл не может быть создан</string>
|
||||
<string name="error_permission_denied">Доступ запрещен системой</string>
|
||||
<string name="error_unknown_host">Сервер не найден</string>
|
||||
<string name="error_http_unsupported_range">"Сервер не поддерживает многопотоковую загрузку, попробуйте с @string/msg_threads = 1"</string>
|
||||
|
|
|
@ -449,12 +449,12 @@
|
|||
<string name="download_finished_more">%s indirme bitti</string>
|
||||
<string name="generate_unique_name">Benzersiz ad oluştur</string>
|
||||
<string name="overwrite">Üzerine yaz</string>
|
||||
<string name="overwrite_warning">Bu ada sahip indirilen bir dosya zaten var</string>
|
||||
<string name="overwrite_finished_warning">Bu ada sahip indirilen bir dosya zaten var</string>
|
||||
<string name="download_already_running">Bu ad ile devam eden bir indirme var</string>
|
||||
<string name="show_error">Hatayı göster</string>
|
||||
<string name="label_code">Kod</string>
|
||||
<string name="error_path_creation">Dosya oluşturulamıyor</string>
|
||||
<string name="error_file_creation">Hedef klasör oluşturulamıyor</string>
|
||||
<string name="error_file_creation">Dosya oluşturulamıyor</string>
|
||||
<string name="error_path_creation">Hedef klasör oluşturulamıyor</string>
|
||||
<string name="error_permission_denied">İzin sistem tarafından reddedildi</string>
|
||||
<string name="error_ssl_exception">Güvenli bağlantı başarısız</string>
|
||||
<string name="error_unknown_host">Sunucu bulunamadı</string>
|
||||
|
|
|
@ -440,11 +440,11 @@
|
|||
<string name="download_finished_more">%s tải về đã xong</string>
|
||||
<string name="generate_unique_name">Tạo tên riêng biệt</string>
|
||||
<string name="overwrite">Ghi đè</string>
|
||||
<string name="overwrite_warning">Có một tệp đã tải về trùng tên</string>
|
||||
<string name="overwrite_finished_warning">Có một tệp đã tải về trùng tên</string>
|
||||
<string name="download_already_running">Có một tệp trùng tên đang tải về</string>
|
||||
<string name="show_error">Hiện lỗi</string>
|
||||
<string name="error_path_creation">Không thể tạo tệp</string>
|
||||
<string name="error_file_creation">Không thể tạo thư mục đích</string>
|
||||
<string name="error_file_creation">Không thể tạo tệp</string>
|
||||
<string name="error_path_creation">Không thể tạo thư mục đích</string>
|
||||
<string name="error_permission_denied">Quyền bị từ chối bởi hệ thống</string>
|
||||
<string name="error_ssl_exception">Không thể tạo kết nối an toàn</string>
|
||||
<string name="error_unknown_host">Không thể tìm máy chủ</string>
|
||||
|
|
|
@ -445,12 +445,12 @@
|
|||
<string name="download_finished_more">%s 個下載已結束</string>
|
||||
<string name="generate_unique_name">生成獨特的名稱</string>
|
||||
<string name="overwrite">覆寫</string>
|
||||
<string name="overwrite_warning">已有此名稱的已下載檔案</string>
|
||||
<string name="overwrite_finished_warning">已有此名稱的已下載檔案</string>
|
||||
<string name="download_already_running">已有此名稱的當案的下載正在進行</string>
|
||||
<string name="show_error">顯示錯誤</string>
|
||||
<string name="label_code">代碼</string>
|
||||
<string name="error_path_creation">無法建立檔案</string>
|
||||
<string name="error_file_creation">無法建立目的地資料夾</string>
|
||||
<string name="error_file_creation">無法建立檔案</string>
|
||||
<string name="error_path_creation">無法建立目的地資料夾</string>
|
||||
<string name="error_permission_denied">被系統拒絕的權限</string>
|
||||
<string name="error_ssl_exception">安全連線失敗</string>
|
||||
<string name="error_unknown_host">找不到伺服器</string>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<string name="saved_tabs_key" translatable="false">saved_tabs_key</string>
|
||||
|
||||
<!-- Key values -->
|
||||
<string name="download_path_key" translatable="false">download_path</string>
|
||||
<string name="download_path_video_key" translatable="false">download_path</string>
|
||||
<string name="download_path_audio_key" translatable="false">download_path_audio</string>
|
||||
|
||||
<string name="use_external_video_player_key" translatable="false">use_external_video_player</string>
|
||||
|
@ -160,6 +160,21 @@
|
|||
<string name="clear_views_history_key" translatable="false">clear_play_history</string>
|
||||
<string name="clear_search_history_key" translatable="false">clear_search_history</string>
|
||||
|
||||
<string name="downloads_storage_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 -->
|
||||
<string name="settings_file_charset_key" translatable="false">file_rename</string>
|
||||
<string name="settings_file_replacement_character_key" translatable="false">file_replacement_character</string>
|
||||
|
|
|
@ -176,7 +176,7 @@
|
|||
<!-- error strings -->
|
||||
<string name="general_error">Error</string>
|
||||
<string name="download_to_sdcard_error_title">External storage unavailable</string>
|
||||
<string name="download_to_sdcard_error_message">Downloading to external SD card not yet possible. Reset download folder location\?</string>
|
||||
<string name="download_to_sdcard_error_message">Downloading to external SD card not possible. Reset download folder location\?</string>
|
||||
<string name="network_error">Network error</string>
|
||||
<string name="could_not_load_thumbnails">Could not load all thumbnails</string>
|
||||
<string name="youtube_signature_decryption_error">Could not decrypt video URL signature</string>
|
||||
|
@ -512,15 +512,17 @@
|
|||
<!-- dialog about existing downloads -->
|
||||
<string name="generate_unique_name">Generate unique name</string>
|
||||
<string name="overwrite">Overwrite</string>
|
||||
<string name="overwrite_warning">A downloaded file with this name already exists</string>
|
||||
<string name="overwrite_unrelated_warning">A file with this name already exists</string>
|
||||
<string name="overwrite_finished_warning">A downloaded file with this name already exists</string>
|
||||
<string name="overwrite_failed">cannot overwrite the file</string>
|
||||
<string name="download_already_running">There is a download in progress with this name</string>
|
||||
<string name="download_already_pending">There is a pending download with this name</string>
|
||||
|
||||
<!-- message dialog about download error -->
|
||||
<string name="show_error">Show error</string>
|
||||
<string name="label_code">Code</string>
|
||||
<string name="error_path_creation">The file can not be created</string>
|
||||
<string name="error_file_creation">The destination folder can not be created</string>
|
||||
<string name="error_file_creation">The file can not be created</string>
|
||||
<string name="error_path_creation">The destination folder can not be created</string>
|
||||
<string name="error_permission_denied">Permission denied by the system</string>
|
||||
<string name="error_ssl_exception">Secure connection failed</string>
|
||||
<string name="error_unknown_host">Could not find the server</string>
|
||||
|
@ -532,6 +534,7 @@
|
|||
<string name="error_postprocessing_failed">Post-processing failed</string>
|
||||
<string name="error_postprocessing_stopped">NewPipe was closed while working on the file</string>
|
||||
<string name="error_insufficient_storage">No space left on device</string>
|
||||
<string name="error_progress_lost">Progress lost, because the file was deleted</string>
|
||||
|
||||
<string name="clear_finished_download">Clear finished 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="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>
|
|
@ -4,10 +4,26 @@
|
|||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
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
|
||||
app:iconSpaceReserved="false"
|
||||
android:dialogTitle="@string/download_path_dialog_title"
|
||||
android:key="@string/download_path_key"
|
||||
android:key="@string/download_path_video_key"
|
||||
android:summary="@string/download_path_summary"
|
||||
android:title="@string/download_path_title"/>
|
||||
|
||||
|
|
Loading…
Reference in a new issue