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:
kapodamy 2019-04-05 14:45:39 -03:00
parent 9e34fee58c
commit f6b32823ba
62 changed files with 2439 additions and 1180 deletions

View file

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

View file

@ -1,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,119 +501,248 @@ 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;
}
// check for existing file with the same name
Uri result = mainStorage.findFile(filename);
if (result == null) {
// the file does not exists, create
StoredFileHelper storage = mainStorage.createFile(filename, mime);
if (storage == null || !storage.canWrite()) {
showFailedDialog(R.string.error_file_creation);
return;
}
continueSelectedDownload(storage);
return;
}
// the target filename is already use, try load
StoredFileHelper storage;
try {
storage = new StoredFileHelper(context, result, mime);
} catch (IOException e) {
showErrorActivity(e);
return;
}
// check if is our file
MissionState state = downloadManager.checkForExistingMission(storage);
@StringRes int msgBtn;
@StringRes int msgBody;
switch (state) {
case Finished:
msgBtn = R.string.overwrite;
msgBody = R.string.overwrite_finished_warning;
break;
case Pending:
msgBtn = R.string.overwrite;
msgBody = R.string.download_already_pending;
break;
case PendingRunning:
msgBtn = R.string.generate_unique_name;
msgBody = R.string.download_already_running;
break;
case None:
msgBtn = R.string.overwrite;
msgBody = R.string.overwrite_unrelated_warning;
break;
default:
return;
}
int threads;
// handle user answer (overwrite or create another file with different name)
final String finalFilename = filename;
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(R.string.download_dialog_title)
.setMessage(msgBody)
.setPositiveButton(msgBtn, (dialog, which) -> {
dialog.dismiss();
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
threads = 1;// use unique thread for subtitles due small file size
fileName += ".srt";// final subtitle format
} else {
threads = threadsSeekBar.getProgress() + 1;
fileName += "." + stream.getFormat().getSuffix();
}
final String finalFileName = fileName;
DownloadManagerService.checkForRunningMission(context, location, fileName, (MissionCheck result) -> {
@StringRes int msgBtn;
@StringRes int msgBody;
switch (result) {
case Finished:
msgBtn = R.string.overwrite;
msgBody = R.string.overwrite_warning;
break;
case Pending:
msgBtn = R.string.overwrite;
msgBody = R.string.download_already_pending;
break;
case PendingRunning:
msgBtn = R.string.generate_unique_name;
msgBody = R.string.download_already_running;
break;
default:
downloadSelected(context, stream, location, finalFileName, kind, threads);
return;
}
// overwrite or unique name actions are done by the download manager
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(R.string.download_dialog_title)
.setMessage(msgBody)
.setPositiveButton(
msgBtn,
(dialog, which) -> downloadSelected(context, stream, location, finalFileName, kind, threads)
)
.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel())
.create()
.show();
});
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) {
if (selectedStream.getFormat() == MediaFormat.M4A) {
psName = Postprocessing.ALGORITHM_M4A_NO_DASH;
}
} else if (selectedStream instanceof VideoStream) {
SecondaryStreamHelper<AudioStream> secondaryStream = videoStreamsAdapter
.getAllSecondary()
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
// 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 (secondaryStream != null) {
secondaryStreamUrl = secondaryStream.getStream().getUrl();
psName = selectedStream.getFormat() == MediaFormat.MPEG_4 ? Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER : Postprocessing.ALGORITHM_WEBM_MUXER;
psArgs = null;
long videoSize = wrappedVideoStreams.getSizeInBytes((VideoStream) selectedStream);
// set nearLength, only, if both sizes are fetched or known. this probably does not work on slow networks
if (secondaryStream.getSizeInBytes() > 0 && videoSize > 0) {
nearLength = secondaryStream.getSizeInBytes() + videoSize;
if (selectedStream.getFormat() == MediaFormat.M4A) {
psName = Postprocessing.ALGORITHM_M4A_NO_DASH;
}
}
} else if ((selectedStream instanceof SubtitlesStream) && selectedStream.getFormat() == MediaFormat.TTML) {
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
psArgs = new String[]{
selectedStream.getFormat().getSuffix(),
"false",// ignore empty frames
"false",// detect youtube duplicate lines
};
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();
if (selectedStream.getFormat() == MediaFormat.MPEG_4)
psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER;
else
psName = Postprocessing.ALGORITHM_WEBM_MUXER;
psArgs = null;
long videoSize = wrappedVideoStreams.getSizeInBytes((VideoStream) selectedStream);
// set nearLength, only, if both sizes are fetched or known. this probably does not work on slow networks
if (secondaryStream.getSizeInBytes() > 0 && videoSize > 0) {
nearLength = secondaryStream.getSizeInBytes() + videoSize;
}
}
break;
case R.id.subtitle_button:
kind = 's';
selectedStream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
if (selectedStream.getFormat() == MediaFormat.TTML) {
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
psArgs = new String[]{
selectedStream.getFormat().getSuffix(),
"false",// ignore empty frames
"false",// detect youtube duplicate lines
};
}
break;
default:
return;
}
if (secondaryStreamUrl == null) {
@ -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();
}
}

View file

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

View file

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

View file

@ -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)
.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);
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);
}
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);
}
}
}

View file

@ -70,37 +70,23 @@ public class NewPipeSettings {
getAudioDownloadFolder(context);
}
public static File getVideoDownloadFolder(Context context) {
return getDir(context, R.string.download_path_key, Environment.DIRECTORY_MOVIES);
private static void getVideoDownloadFolder(Context context) {
getDir(context, R.string.download_path_video_key, Environment.DIRECTORY_MOVIES);
}
public static String getVideoDownloadPath(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final String key = context.getString(R.string.download_path_key);
return prefs.getString(key, Environment.DIRECTORY_MOVIES);
private static void getAudioDownloadFolder(Context context) {
getDir(context, R.string.download_path_audio_key, Environment.DIRECTORY_MUSIC);
}
public static File getAudioDownloadFolder(Context context) {
return getDir(context, R.string.download_path_audio_key, Environment.DIRECTORY_MUSIC);
}
public static String getAudioDownloadPath(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final String key = context.getString(R.string.download_path_audio_key);
return prefs.getString(key, Environment.DIRECTORY_MUSIC);
}
private static File getDir(Context context, int keyID, String defaultDirectoryName) {
private static void getDir(Context context, int keyID, String defaultDirectoryName) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final String key = context.getString(keyID);
String downloadPath = prefs.getString(key, null);
if ((downloadPath != null) && (!downloadPath.isEmpty())) return new File(downloadPath.trim());
if ((downloadPath != null) && (!downloadPath.isEmpty())) return;
final File dir = getDir(defaultDirectoryName);
SharedPreferences.Editor spEditor = prefs.edit();
spEditor.putString(key, getNewPipeChildFolderPathForDir(dir));
spEditor.putString(key, getNewPipeChildFolderPathForDir(getDir(defaultDirectoryName)));
spEditor.apply();
return dir;
}
@NonNull
@ -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) {

View file

@ -120,7 +120,7 @@ public class Mp4FromDashWriter {
parsed = true;
for (SharpStream src : sourceTracks) {
src.dispose();
src.close();
}
tracks = null;

View file

@ -107,7 +107,7 @@ public class WebMWriter {
parsed = true;
for (SharpStream src : sourceTracks) {
src.dispose();
src.close();
}
sourceTracks = null;

View file

@ -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");
}
}

View file

@ -3,10 +3,10 @@ package us.shandian.giga.get;
import android.support.annotation.NonNull;
import android.util.Log;
import java.io.File;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.nio.channels.ClosedByInterruptException;
@ -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;

View file

@ -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();

View file

@ -2,9 +2,10 @@ package us.shandian.giga.get;
import android.util.Log;
import java.io.FileNotFoundException;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.nio.channels.ClosedByInterruptException;
@ -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;
}

View file

@ -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?
}
if (mF != null) mF.close();
}
@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();

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

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

View file

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

View file

@ -0,0 +1,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);
}
}
}

View file

@ -1,150 +1,148 @@
package us.shandian.giga.postprocessing.io;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
public class ChunkFileInputStream extends SharpStream {
private RandomAccessFile source;
private final long offset;
private final long length;
private long position;
public ChunkFileInputStream(File file, long start, long end, String mode) throws IOException {
source = new RandomAccessFile(file, mode);
offset = start;
length = end - start;
position = 0;
if (length < 1) {
source.close();
throw new IOException("The chunk is empty or invalid");
}
if (source.length() < end) {
try {
throw new IOException(String.format("invalid file length. expected = %s found = %s", end, source.length()));
} finally {
source.close();
}
}
source.seek(offset);
}
/**
* Get absolute position on file
*
* @return the position
*/
public long getFilePointer() {
return offset + position;
}
@Override
public int read() throws IOException {
if ((position + 1) > length) {
return 0;
}
int res = source.read();
if (res >= 0) {
position++;
}
return res;
}
@Override
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
@Override
public int read(byte b[], int off, int len) throws IOException {
if ((position + len) > length) {
len = (int) (length - position);
}
if (len == 0) {
return 0;
}
int res = source.read(b, off, len);
position += res;
return res;
}
@Override
public long skip(long pos) throws IOException {
pos = Math.min(pos + position, length);
if (pos == 0) {
return 0;
}
source.seek(offset + pos);
long oldPos = position;
position = pos;
return pos - oldPos;
}
@Override
public long available() {
return (int) (length - position);
}
@SuppressWarnings("EmptyCatchBlock")
@Override
public void dispose() {
try {
source.close();
} catch (IOException err) {
} finally {
source = null;
}
}
@Override
public boolean isDisposed() {
return source == null;
}
@Override
public void rewind() throws IOException {
position = 0;
source.seek(offset);
}
@Override
public boolean canRewind() {
return true;
}
@Override
public boolean canRead() {
return true;
}
@Override
public boolean canWrite() {
return false;
}
@Override
public void write(byte value) {
}
@Override
public void write(byte[] buffer) {
}
@Override
public void write(byte[] buffer, int offset, int count) {
}
}
package us.shandian.giga.io;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
public class ChunkFileInputStream extends SharpStream {
private SharpStream source;
private final long offset;
private final long length;
private long position;
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;
if (length < 1) {
source.close();
throw new IOException("The chunk is empty or invalid");
}
if (source.length() < end) {
try {
throw new IOException(String.format("invalid file length. expected = %s found = %s", end, source.length()));
} finally {
source.close();
}
}
source.seek(offset);
}
/**
* Get absolute position on file
*
* @return the position
*/
public long getFilePointer() {
return offset + position;
}
@Override
public int read() throws IOException {
if ((position + 1) > length) {
return 0;
}
int res = source.read();
if (res >= 0) {
position++;
}
return res;
}
@Override
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
@Override
public int read(byte b[], int off, int len) throws IOException {
if ((position + len) > length) {
len = (int) (length - position);
}
if (len == 0) {
return 0;
}
int res = source.read(b, off, len);
position += res;
return res;
}
@Override
public long skip(long pos) throws IOException {
pos = Math.min(pos + position, length);
if (pos == 0) {
return 0;
}
source.seek(offset + pos);
long oldPos = position;
position = pos;
return pos - oldPos;
}
@Override
public long available() {
return (int) (length - position);
}
@SuppressWarnings("EmptyCatchBlock")
@Override
public void close() {
source.close();
source = null;
}
@Override
public boolean isClosed() {
return source == null;
}
@Override
public void rewind() throws IOException {
position = 0;
source.seek(offset);
}
@Override
public boolean canRewind() {
return true;
}
@Override
public boolean canRead() {
return true;
}
@Override
public boolean canWrite() {
return false;
}
@Override
public void write(byte value) {
}
@Override
public void write(byte[] buffer) {
}
@Override
public void write(byte[] buffer, int offset, int count) {
}
}

View file

@ -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 {
queue = null;
target.close();
} catch (IOException e) {
// nothing to do
}
protected void close() {
queue = null;
target.close();
}
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
);
}
}

View 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();
}
}

View 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);
}
}

View file

@ -1,61 +1,61 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package us.shandian.giga.postprocessing.io;
import android.support.annotation.NonNull;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
import java.io.InputStream;
/**
* Wrapper for the classic {@link java.io.InputStream}
*
* @author kapodamy
*/
public class SharpInputStream extends InputStream {
private final SharpStream base;
public SharpInputStream(SharpStream base) throws IOException {
if (!base.canRead()) {
throw new IOException("The provided stream is not readable");
}
this.base = base;
}
@Override
public int read() throws IOException {
return base.read();
}
@Override
public int read(@NonNull byte[] bytes) throws IOException {
return base.read(bytes);
}
@Override
public int read(@NonNull byte[] bytes, int i, int i1) throws IOException {
return base.read(bytes, i, i1);
}
@Override
public long skip(long l) throws IOException {
return base.skip(l);
}
@Override
public int available() {
long res = base.available();
return res > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) res;
}
@Override
public void close() {
base.dispose();
}
}
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package us.shandian.giga.io;
import android.support.annotation.NonNull;
import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
import java.io.InputStream;
/**
* Wrapper for the classic {@link java.io.InputStream}
*
* @author kapodamy
*/
public class SharpInputStream extends InputStream {
private final SharpStream base;
public SharpInputStream(SharpStream base) throws IOException {
if (!base.canRead()) {
throw new IOException("The provided stream is not readable");
}
this.base = base;
}
@Override
public int read() throws IOException {
return base.read();
}
@Override
public int read(@NonNull byte[] bytes) throws IOException {
return base.read(bytes);
}
@Override
public int read(@NonNull byte[] bytes, int i, int i1) throws IOException {
return base.read(bytes, i, i1);
}
@Override
public long skip(long l) throws IOException {
return base.skip(l);
}
@Override
public int available() {
long res = base.available();
return res > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) res;
}
@Override
public void close() {
base.close();
}
}

View 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();
}
}

View 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;
}
}

View file

@ -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

View file

@ -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

View file

@ -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();
}
}

View file

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

View file

@ -7,15 +7,13 @@ import org.schabi.newpipe.streams.io.SharpStream;
import java.io.IOException;
import us.shandian.giga.get.DownloadMission;
/**
* @author kapodamy
*/
class WebMMuxer extends Postprocessing {
WebMMuxer(DownloadMission mission) {
super(mission, 2048 * 1024/* 2 MiB */, true);
WebMMuxer() {
super(2048 * 1024/* 2 MiB */, true);
}
@Override

View file

@ -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());
}
mDownloadDataSource.deleteMission(mission);
continue;
if (!mission.storage.existsAsFile()) {
if (DEBUG) Log.d(TAG, "downloaded file removed: " + mission.storage.getName());
mFinishedMissionStore.deleteMission(mission);
finishedMissions.remove(i);
}
result.add(mission);
}
return result;
return finishedMissions;
}
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());
} 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 (mis.isInitialized()) {
// the progress is lost, reset mission state
DownloadMission m = new DownloadMission(mis.urls, mis.storage, mis.kind, mis.psAlgorithm);
m.timestamp = mis.timestamp;
m.threadCount = mis.threadCount;
m.source = mis.source;
m.nearLength = mis.nearLength;
m.enqueued = mis.enqueued;
m.errCode = DownloadMission.ERROR_PROGRESS_LOST;
mis = m;
}
continue;
}
if (!exists && mis.isInitialized()) {
// downloaded file deleted, reset mission state
DownloadMission m = new DownloadMission(mis.urls, mis.name, mis.location, mis.kind, mis.postprocessingName, mis.postprocessingArgs);
m.timestamp = mis.timestamp;
m.threadCount = mis.threadCount;
m.source = mis.source;
m.nearLength = mis.nearLength;
m.setEnqueued(mis.enqueued);
mis = m;
}
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;
}
}

View file

@ -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) {
try {
((DMBinder) service).getDownloadManager().checkForRunningMission(location, name, checker);
} catch (Exception err) {
Log.w(TAG, "checkForRunningMission() callback is defective", err);
}
StoredFileHelper storage;
try {
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}
}

View file

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

View file

@ -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(
mContext,
BuildConfig.APPLICATION_ID + ".provider",
mission.getDownloadedFile()
);
Uri uri;
if (mission.storage.isDirect()) {
uri = FileProvider.getUriForFile(
mContext,
BuildConfig.APPLICATION_ID + ".provider",
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);
}
}

View file

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

View file

@ -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();
}
}
}

View file

@ -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) {

View file

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

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -14,7 +14,7 @@
<string name="share_dialog_title">Compartir con</string>
<string name="choose_browser">Elegir navegador</string>
<string name="screen_rotation">rotación</string>
<string name="download_path_title">Ruta de descarga de vídeo</string>
<string name="download_path_title">Carpeta de descarga de vídeo</string>
<string name="download_path_summary">Ruta para almacenar los vídeos descargados</string>
<string name="download_path_dialog_title">Introducir directorio de descargas para vídeos</string>
<string name="default_resolution_title">Resolución por defecto de vídeo</string>
@ -40,7 +40,7 @@
<string name="use_tor_summary">(Experimental) Forzar la descarga a través de Tor para una mayor privacidad (transmisión de vídeos aún no compatible).</string>
<string name="err_dir_create">No se puede crear la carpeta de descarga \'%1$s\'</string>
<string name="info_dir_created">Carpeta de descarga creada \'%1$s\'</string>
<string name="download_path_audio_summary">Los audios descargados se almacenan aquí</string>
<string name="download_path_audio_summary">Ruta para almacenar los audios descargados</string>
<string name="download_path_audio_dialog_title">Introducir ruta de descarga para archivos de audio</string>
<string name="blocked_by_gema">Bloqueado por GEMA</string>
<string name="download_path_audio_title">Carpeta de descarga de audio</string>
@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -447,12 +447,12 @@
<string name="download_finished_more">%s unduhan selesai</string>
<string name="generate_unique_name">Hasilkan nama unik</string>
<string name="overwrite">Timpa</string>
<string name="overwrite_warning">File yang diunduh dengan nama ini sudah ada</string>
<string name="overwrite_finished_warning">File yang diunduh dengan nama ini sudah ada</string>
<string name="download_already_running">Ada unduhan yang sedang berlangsung dengan nama ini</string>
<string name="show_error">Tunjukkan kesalahan</string>
<string name="label_code">Kode</string>
<string name="error_path_creation">File tidak dapat dibuat</string>
<string name="error_file_creation">Folder tujuan tidak dapat dibuat</string>
<string name="error_file_creation">File tidak dapat dibuat</string>
<string name="error_path_creation">Folder tujuan tidak dapat dibuat</string>
<string name="error_permission_denied">Izin ditolak oleh sistem</string>
<string name="error_ssl_exception">Koneksi aman gagal</string>
<string name="error_unknown_host">Tidak dapat menemukan server</string>

View file

@ -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>

View file

@ -365,10 +365,10 @@
<string name="caption_auto_generated">自動生成</string>
<string name="caption_setting_description">アプリの再起動後、設定した字幕設定が反映されます</string>
<string name="empty_subscription_feed_subtitle">何もありません</string>
<string name="import_youtube_instructions">保存したエクスポートファイルからYouTubeの購読をインポート:
\n
\n1. このURLを開きます: %1$s
\n2. ログインしていなければログインします
<string name="import_youtube_instructions">保存したエクスポートファイルからYouTubeの購読をインポート:
\n
\n1. このURLを開きます: %1$s
\n2. ログインしていなければログインします
\n3. ダウンロードが始まります (これがエクスポートファイルです)</string>
<string name="playback_reset">リセット</string>
<string name="accept">同意する</string>
@ -391,8 +391,8 @@
\n3. 必要に応じてログインします
\n4. リダイレクトされたプロファイル URL をコピーします。</string>
<string name="import_soundcloud_instructions_hint">あなたのID, soundcloud.com/あなたのid</string>
<string name="import_network_expensive_warning">この操作により通信料金が増えることがあります。ご注意ください。
\n
<string name="import_network_expensive_warning">この操作により通信料金が増えることがあります。ご注意ください。
\n
\n続行しますか\?</string>
<string name="playback_speed_control">再生速度を変更</string>
<string name="unhook_checkbox">速度と音程を連動せずに変更 (歪むかもしれません)</string>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

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

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -11,7 +11,7 @@
<string name="saved_tabs_key" translatable="false">saved_tabs_key</string>
<!-- Key values -->
<string name="download_path_key" translatable="false">download_path</string>
<string name="download_path_video_key" translatable="false">download_path</string>
<string name="download_path_audio_key" translatable="false">download_path_audio</string>
<string name="use_external_video_player_key" translatable="false">use_external_video_player</string>
@ -160,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>

View file

@ -176,7 +176,7 @@
<!-- error strings -->
<string name="general_error">Error</string>
<string name="download_to_sdcard_error_title">External storage unavailable</string>
<string name="download_to_sdcard_error_message">Downloading to external SD card not yet possible. Reset download folder location\?</string>
<string name="download_to_sdcard_error_message">Downloading to external SD card not possible. Reset download folder location\?</string>
<string name="network_error">Network error</string>
<string name="could_not_load_thumbnails">Could not load all thumbnails</string>
<string name="youtube_signature_decryption_error">Could not decrypt video URL signature</string>
@ -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>

View file

@ -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"/>