mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2024-06-27 07:33:20 +00:00
210834fbe9
Detailed changes: - External players: - Add a message instruction about stream selection; - Add a message when there is no stream available for external players; - Return now HLS, DASH and SmoothStreaming URL contents, in addition to progressive HTTP ones. - Player: - Support DASH, HLS and SmoothStreaming streams for videos, whether they are content URLs or the manifests themselves, in addition to progressive HTTP ones; - Use a custom HttpDataSource to play YouTube contents, based of ExoPlayer's default one, which allows better spoofing of official clients (custom user-agent and headers (depending of the client used), use of range and rn (set dynamically by the DataSource) parameters); - Fetch YouTube progressive contents as DASH streams, like official clients, support fully playback of livestreams which have ended recently and OTF streams; - Use ExoPlayer's default retries count for contents on non-fatal errors (instead of Integer.MAX_VALUE for non-live contents and 5 for live contents). - Download dialog: - Add message about support of progressive HTTP streams only for downloading; - Remove several duplicated code and update relevant usages; - Support downloading of contents with an unknown media format. - ListHelper: - Catch NumberFormatException when trying to compare two video streams between them. - Tests: - Update ListHelperTest and StreamItemAdapterTest to fix breaking changes in the extractor. - Other places: - Fixes deprecation of changes made in the extractor; - Improve some code related to the files changed. - Issues fixed and/or improved with the changes: - Seeking of PeerTube HLS streams (the duration shown was the one from the stream duration and not the one parsed, incomplete because HLS streams are fragmented MP4s with multiple sidx boxes, for which seeking is not supported by ExoPlayer) (the app now uses the HLS manifest returned for each quality, in the master playlist (not fetched and computed by the extractor)); - Crash when loading PeerTube streams with a separated audio; - Lack of some streams on some YouTube videos (OTF streams); - Loading times of YouTube streams, after a quality change or a playback start; - View count of YouTube ended livestreams interpreted as watching count (this type of streams is not interpreted anymore as livestreams); - Watchable time of YouTube ended livestreams; - Playback of SoundCloud HLS-only tracks (which cannot be downloaded anymore because the workaround which was used is being removed by SoundCloud, so it has been removed from the extractor).
1113 lines
45 KiB
Java
1113 lines
45 KiB
Java
package org.schabi.newpipe.download;
|
|
|
|
import android.app.Activity;
|
|
import android.content.ComponentName;
|
|
import android.content.Context;
|
|
import android.content.DialogInterface;
|
|
import android.content.DialogInterface.OnDismissListener;
|
|
import android.content.Intent;
|
|
import android.content.ServiceConnection;
|
|
import android.content.SharedPreferences;
|
|
import android.net.Uri;
|
|
import android.os.Bundle;
|
|
import android.os.Environment;
|
|
import android.os.IBinder;
|
|
import android.util.Log;
|
|
import android.util.SparseArray;
|
|
import android.view.LayoutInflater;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
import android.widget.AdapterView;
|
|
import android.widget.RadioGroup;
|
|
import android.widget.SeekBar;
|
|
import android.widget.Toast;
|
|
|
|
import androidx.activity.result.ActivityResult;
|
|
import androidx.activity.result.ActivityResultLauncher;
|
|
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
|
|
import androidx.annotation.IdRes;
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
import androidx.annotation.StringRes;
|
|
import androidx.appcompat.app.AlertDialog;
|
|
import androidx.appcompat.view.menu.ActionMenuItemView;
|
|
import androidx.appcompat.widget.Toolbar;
|
|
import androidx.documentfile.provider.DocumentFile;
|
|
import androidx.fragment.app.DialogFragment;
|
|
import androidx.preference.PreferenceManager;
|
|
|
|
import com.nononsenseapps.filepicker.Utils;
|
|
|
|
import org.schabi.newpipe.MainActivity;
|
|
import org.schabi.newpipe.R;
|
|
import org.schabi.newpipe.databinding.DownloadDialogBinding;
|
|
import org.schabi.newpipe.error.ErrorInfo;
|
|
import org.schabi.newpipe.error.ErrorUtil;
|
|
import org.schabi.newpipe.error.UserAction;
|
|
import org.schabi.newpipe.extractor.MediaFormat;
|
|
import org.schabi.newpipe.extractor.NewPipe;
|
|
import org.schabi.newpipe.extractor.localization.Localization;
|
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
|
import org.schabi.newpipe.extractor.stream.DeliveryMethod;
|
|
import org.schabi.newpipe.extractor.stream.Stream;
|
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
|
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
|
import org.schabi.newpipe.extractor.stream.VideoStream;
|
|
import org.schabi.newpipe.settings.NewPipeSettings;
|
|
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
|
|
import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
|
|
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
|
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
|
import org.schabi.newpipe.util.FilenameUtils;
|
|
import org.schabi.newpipe.util.ListHelper;
|
|
import org.schabi.newpipe.util.PermissionHelper;
|
|
import org.schabi.newpipe.util.SecondaryStreamHelper;
|
|
import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
|
|
import org.schabi.newpipe.util.StreamItemAdapter;
|
|
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
|
|
import org.schabi.newpipe.util.ThemeHelper;
|
|
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Objects;
|
|
|
|
import icepick.Icepick;
|
|
import icepick.State;
|
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
|
import us.shandian.giga.get.MissionRecoveryInfo;
|
|
import us.shandian.giga.postprocessing.Postprocessing;
|
|
import us.shandian.giga.service.DownloadManager;
|
|
import us.shandian.giga.service.DownloadManagerService;
|
|
import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder;
|
|
import us.shandian.giga.service.MissionState;
|
|
|
|
import static org.schabi.newpipe.util.ListHelper.keepStreamsWithDelivery;
|
|
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
|
|
|
public class DownloadDialog extends DialogFragment
|
|
implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
|
|
private static final String TAG = "DialogFragment";
|
|
private static final boolean DEBUG = MainActivity.DEBUG;
|
|
|
|
@State
|
|
StreamInfo currentInfo;
|
|
@State
|
|
public StreamSizeWrapper<AudioStream> wrappedAudioStreams = StreamSizeWrapper.empty();
|
|
@State
|
|
public StreamSizeWrapper<VideoStream> wrappedVideoStreams = StreamSizeWrapper.empty();
|
|
@State
|
|
public StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams = StreamSizeWrapper.empty();
|
|
@State
|
|
int selectedVideoIndex = 0;
|
|
@State
|
|
int selectedAudioIndex = 0;
|
|
@State
|
|
int selectedSubtitleIndex = 0;
|
|
|
|
@Nullable
|
|
private OnDismissListener onDismissListener = null;
|
|
|
|
private StoredDirectoryHelper mainStorageAudio = null;
|
|
private StoredDirectoryHelper mainStorageVideo = null;
|
|
private DownloadManager downloadManager = null;
|
|
private ActionMenuItemView okButton = null;
|
|
private Context context;
|
|
private boolean askForSavePath;
|
|
|
|
private StreamItemAdapter<AudioStream, Stream> audioStreamsAdapter;
|
|
private StreamItemAdapter<VideoStream, AudioStream> videoStreamsAdapter;
|
|
private StreamItemAdapter<SubtitlesStream, Stream> subtitleStreamsAdapter;
|
|
|
|
private final CompositeDisposable disposables = new CompositeDisposable();
|
|
|
|
private DownloadDialogBinding dialogBinding;
|
|
|
|
private SharedPreferences prefs;
|
|
|
|
// Variables for file name and MIME type when picking new folder because it's not set yet
|
|
private String filenameTmp;
|
|
private String mimeTmp;
|
|
|
|
private final ActivityResultLauncher<Intent> requestDownloadSaveAsLauncher =
|
|
registerForActivityResult(
|
|
new StartActivityForResult(), this::requestDownloadSaveAsResult);
|
|
private final ActivityResultLauncher<Intent> requestDownloadPickAudioFolderLauncher =
|
|
registerForActivityResult(
|
|
new StartActivityForResult(), this::requestDownloadPickAudioFolderResult);
|
|
private final ActivityResultLauncher<Intent> requestDownloadPickVideoFolderLauncher =
|
|
registerForActivityResult(
|
|
new StartActivityForResult(), this::requestDownloadPickVideoFolderResult);
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Instance creation
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
@NonNull
|
|
public static DownloadDialog newInstance(final Context context,
|
|
@NonNull final StreamInfo info) {
|
|
// TODO: Adapt this code when the downloader support other types of stream deliveries
|
|
final List<VideoStream> videoStreams = new ArrayList<>(info.getVideoStreams());
|
|
final List<VideoStream> progressiveHttpVideoStreams =
|
|
keepStreamsWithDelivery(videoStreams, DeliveryMethod.PROGRESSIVE_HTTP);
|
|
|
|
final List<VideoStream> videoOnlyStreams = new ArrayList<>(info.getVideoOnlyStreams());
|
|
final List<VideoStream> progressiveHttpVideoOnlyStreams =
|
|
keepStreamsWithDelivery(videoOnlyStreams, DeliveryMethod.PROGRESSIVE_HTTP);
|
|
|
|
final List<AudioStream> audioStreams = new ArrayList<>(info.getAudioStreams());
|
|
final List<AudioStream> progressiveHttpAudioStreams =
|
|
keepStreamsWithDelivery(audioStreams, DeliveryMethod.PROGRESSIVE_HTTP);
|
|
|
|
final List<SubtitlesStream> subtitlesStreams = new ArrayList<>(info.getSubtitles());
|
|
final List<SubtitlesStream> progressiveHttpSubtitlesStreams =
|
|
keepStreamsWithDelivery(subtitlesStreams, DeliveryMethod.PROGRESSIVE_HTTP);
|
|
|
|
final List<VideoStream> videoStreamsList = new ArrayList<>(
|
|
ListHelper.getSortedStreamVideosList(context, progressiveHttpVideoStreams,
|
|
progressiveHttpVideoOnlyStreams, false, false));
|
|
|
|
final DownloadDialog instance = new DownloadDialog();
|
|
instance.setInfo(info);
|
|
instance.setVideoStreams(videoStreamsList);
|
|
instance.setAudioStreams(progressiveHttpAudioStreams);
|
|
instance.setSubtitleStreams(progressiveHttpSubtitlesStreams);
|
|
|
|
return instance;
|
|
}
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Setters
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
private void setInfo(@NonNull final StreamInfo info) {
|
|
this.currentInfo = info;
|
|
}
|
|
|
|
public void setAudioStreams(@NonNull final List<AudioStream> audioStreams) {
|
|
this.wrappedAudioStreams = new StreamSizeWrapper<>(audioStreams, getContext());
|
|
}
|
|
|
|
public void setVideoStreams(@NonNull final List<VideoStream> videoStreams) {
|
|
this.wrappedVideoStreams = new StreamSizeWrapper<>(videoStreams, getContext());
|
|
}
|
|
|
|
public void setSubtitleStreams(@NonNull final List<SubtitlesStream> subtitleStreams) {
|
|
this.wrappedSubtitleStreams = new StreamSizeWrapper<>(subtitleStreams, getContext());
|
|
}
|
|
|
|
/**
|
|
* Set the selected video stream, by using its index in the stream list.
|
|
*
|
|
* The index of the select video stream will be not set if this index is not in the bounds
|
|
* of the stream list.
|
|
*
|
|
* @param svi the index of the selected {@link VideoStream}
|
|
*/
|
|
public void setSelectedVideoStream(final int svi) {
|
|
if (selectedStreamIsInBoundsOfWrappedStreams(svi, this.wrappedVideoStreams)) {
|
|
this.selectedVideoIndex = svi;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the selected audio stream, by using its index in the stream list.
|
|
*
|
|
* The index of the select audio stream will be not set if this index is not in the bounds
|
|
* of the stream list.
|
|
*
|
|
* @param sai the index of the selected {@link AudioStream}
|
|
*/
|
|
public void setSelectedAudioStream(final int sai) {
|
|
if (selectedStreamIsInBoundsOfWrappedStreams(sai, this.wrappedAudioStreams)) {
|
|
this.selectedAudioIndex = sai;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the selected subtitles stream, by using its index in the stream list.
|
|
*
|
|
* The index of the select subtitles stream will be not set if this index is not in the bounds
|
|
* of the stream list.
|
|
*
|
|
* @param ssi the index of the selected {@link SubtitlesStream}
|
|
*/
|
|
public void setSelectedSubtitleStream(final int ssi) {
|
|
if (selectedStreamIsInBoundsOfWrappedStreams(ssi, this.wrappedSubtitleStreams)) {
|
|
this.selectedSubtitleIndex = ssi;
|
|
}
|
|
}
|
|
|
|
private boolean selectedStreamIsInBoundsOfWrappedStreams(
|
|
final int selectedIndexStream,
|
|
final StreamSizeWrapper<? extends Stream> wrappedStreams) {
|
|
return selectedIndexStream > 0
|
|
&& selectedIndexStream < wrappedStreams.getStreamsList().size();
|
|
}
|
|
|
|
public void setOnDismissListener(@Nullable final OnDismissListener onDismissListener) {
|
|
this.onDismissListener = onDismissListener;
|
|
}
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Android lifecycle
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
@Override
|
|
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
|
super.onCreate(savedInstanceState);
|
|
if (DEBUG) {
|
|
Log.d(TAG, "onCreate() called with: "
|
|
+ "savedInstanceState = [" + savedInstanceState + "]");
|
|
}
|
|
|
|
if (!PermissionHelper.checkStoragePermissions(getActivity(),
|
|
PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
|
|
dismiss();
|
|
return;
|
|
}
|
|
|
|
context = getContext();
|
|
|
|
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
|
|
Icepick.restoreInstanceState(this, savedInstanceState);
|
|
|
|
final SparseArray<SecondaryStreamHelper<AudioStream>> secondaryStreams
|
|
= new SparseArray<>(4);
|
|
final List<VideoStream> videoStreams = wrappedVideoStreams.getStreamsList();
|
|
|
|
for (int i = 0; i < videoStreams.size(); i++) {
|
|
if (!videoStreams.get(i).isVideoOnly()) {
|
|
continue;
|
|
}
|
|
final AudioStream audioStream = SecondaryStreamHelper
|
|
.getAudioStreamFor(wrappedAudioStreams.getStreamsList(), videoStreams.get(i));
|
|
|
|
if (audioStream != null) {
|
|
secondaryStreams.append(i, new SecondaryStreamHelper<>(wrappedAudioStreams,
|
|
audioStream));
|
|
} else if (DEBUG) {
|
|
final MediaFormat mediaFormat = videoStreams.get(i).getFormat();
|
|
if (mediaFormat != null) {
|
|
Log.w(TAG, "No audio stream candidates for video format "
|
|
+ mediaFormat.name());
|
|
} else {
|
|
Log.w(TAG, "No audio stream candidates for unknown video format");
|
|
}
|
|
}
|
|
}
|
|
|
|
this.videoStreamsAdapter = new StreamItemAdapter<>(context, wrappedVideoStreams,
|
|
secondaryStreams);
|
|
this.audioStreamsAdapter = new StreamItemAdapter<>(context, wrappedAudioStreams);
|
|
this.subtitleStreamsAdapter = new StreamItemAdapter<>(context, wrappedSubtitleStreams);
|
|
|
|
final Intent intent = new Intent(context, DownloadManagerService.class);
|
|
context.startService(intent);
|
|
|
|
context.bindService(intent, new ServiceConnection() {
|
|
@Override
|
|
public void onServiceConnected(final ComponentName cname, final IBinder service) {
|
|
final DownloadManagerBinder mgr = (DownloadManagerBinder) service;
|
|
|
|
mainStorageAudio = mgr.getMainStorageAudio();
|
|
mainStorageVideo = mgr.getMainStorageVideo();
|
|
downloadManager = mgr.getDownloadManager();
|
|
askForSavePath = mgr.askForSavePath();
|
|
|
|
okButton.setEnabled(true);
|
|
|
|
context.unbindService(this);
|
|
}
|
|
|
|
@Override
|
|
public void onServiceDisconnected(final ComponentName name) {
|
|
// nothing to do
|
|
}
|
|
}, Context.BIND_AUTO_CREATE);
|
|
}
|
|
|
|
@Override
|
|
public View onCreateView(@NonNull final LayoutInflater inflater,
|
|
final ViewGroup container,
|
|
final Bundle savedInstanceState) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "onCreateView() called with: "
|
|
+ "inflater = [" + inflater + "], container = [" + container + "], "
|
|
+ "savedInstanceState = [" + savedInstanceState + "]");
|
|
}
|
|
return inflater.inflate(R.layout.download_dialog, container);
|
|
}
|
|
|
|
@Override
|
|
public void onViewCreated(@NonNull final View view,
|
|
@Nullable final Bundle savedInstanceState) {
|
|
super.onViewCreated(view, savedInstanceState);
|
|
dialogBinding = DownloadDialogBinding.bind(view);
|
|
|
|
dialogBinding.fileName.setText(FilenameUtils.createFilename(getContext(),
|
|
currentInfo.getName()));
|
|
selectedAudioIndex = ListHelper
|
|
.getDefaultAudioFormat(getContext(), wrappedAudioStreams.getStreamsList());
|
|
|
|
selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll());
|
|
|
|
dialogBinding.qualitySpinner.setOnItemSelectedListener(this);
|
|
|
|
dialogBinding.videoAudioGroup.setOnCheckedChangeListener(this);
|
|
|
|
initToolbar(dialogBinding.toolbarLayout.toolbar);
|
|
setupDownloadOptions();
|
|
|
|
prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
|
|
|
|
final int threads = prefs.getInt(getString(R.string.default_download_threads), 3);
|
|
dialogBinding.threadsCount.setText(String.valueOf(threads));
|
|
dialogBinding.threads.setProgress(threads - 1);
|
|
dialogBinding.threads.setOnSeekBarChangeListener(new SimpleOnSeekBarChangeListener() {
|
|
@Override
|
|
public void onProgressChanged(@NonNull final SeekBar seekbar,
|
|
final int progress,
|
|
final boolean fromUser) {
|
|
final int newProgress = progress + 1;
|
|
prefs.edit().putInt(getString(R.string.default_download_threads), newProgress)
|
|
.apply();
|
|
dialogBinding.threadsCount.setText(String.valueOf(newProgress));
|
|
}
|
|
});
|
|
|
|
fetchStreamsSize();
|
|
}
|
|
|
|
private void initToolbar(final Toolbar toolbar) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]");
|
|
}
|
|
|
|
toolbar.setTitle(R.string.download_dialog_title);
|
|
toolbar.setNavigationIcon(R.drawable.ic_arrow_back);
|
|
toolbar.inflateMenu(R.menu.dialog_url);
|
|
toolbar.setNavigationOnClickListener(v -> dismiss());
|
|
toolbar.setNavigationContentDescription(R.string.cancel);
|
|
|
|
okButton = toolbar.findViewById(R.id.okay);
|
|
okButton.setEnabled(false); // disable until the download service connection is done
|
|
|
|
toolbar.setOnMenuItemClickListener(item -> {
|
|
if (item.getItemId() == R.id.okay) {
|
|
prepareSelectedDownload();
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public void onDismiss(@NonNull final DialogInterface dialog) {
|
|
super.onDismiss(dialog);
|
|
if (onDismissListener != null) {
|
|
onDismissListener.onDismiss(dialog);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onDestroy() {
|
|
super.onDestroy();
|
|
disposables.clear();
|
|
}
|
|
|
|
@Override
|
|
public void onDestroyView() {
|
|
dialogBinding = null;
|
|
super.onDestroyView();
|
|
}
|
|
|
|
@Override
|
|
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
|
super.onSaveInstanceState(outState);
|
|
Icepick.saveInstanceState(this, outState);
|
|
}
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Video, audio and subtitle spinners
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
private void fetchStreamsSize() {
|
|
disposables.clear();
|
|
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams)
|
|
.subscribe(result -> {
|
|
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
|
|
== R.id.video_button) {
|
|
setupVideoSpinner();
|
|
}
|
|
}, throwable -> ErrorUtil.showSnackbar(context,
|
|
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
|
|
"Downloading video stream size",
|
|
currentInfo.getServiceId()))));
|
|
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams)
|
|
.subscribe(result -> {
|
|
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
|
|
== R.id.audio_button) {
|
|
setupAudioSpinner();
|
|
}
|
|
}, throwable -> ErrorUtil.showSnackbar(context,
|
|
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
|
|
"Downloading audio stream size",
|
|
currentInfo.getServiceId()))));
|
|
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams)
|
|
.subscribe(result -> {
|
|
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
|
|
== R.id.subtitle_button) {
|
|
setupSubtitleSpinner();
|
|
}
|
|
}, throwable -> ErrorUtil.showSnackbar(context,
|
|
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
|
|
"Downloading subtitle stream size",
|
|
currentInfo.getServiceId()))));
|
|
}
|
|
|
|
private void setupAudioSpinner() {
|
|
if (getContext() == null) {
|
|
return;
|
|
}
|
|
|
|
dialogBinding.qualitySpinner.setAdapter(audioStreamsAdapter);
|
|
dialogBinding.qualitySpinner.setSelection(selectedAudioIndex);
|
|
setRadioButtonsState(true);
|
|
}
|
|
|
|
private void setupVideoSpinner() {
|
|
if (getContext() == null) {
|
|
return;
|
|
}
|
|
|
|
dialogBinding.qualitySpinner.setAdapter(videoStreamsAdapter);
|
|
dialogBinding.qualitySpinner.setSelection(selectedVideoIndex);
|
|
setRadioButtonsState(true);
|
|
}
|
|
|
|
private void setupSubtitleSpinner() {
|
|
if (getContext() == null) {
|
|
return;
|
|
}
|
|
|
|
dialogBinding.qualitySpinner.setAdapter(subtitleStreamsAdapter);
|
|
dialogBinding.qualitySpinner.setSelection(selectedSubtitleIndex);
|
|
setRadioButtonsState(true);
|
|
}
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Activity results
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
private void requestDownloadPickAudioFolderResult(final ActivityResult result) {
|
|
requestDownloadPickFolderResult(
|
|
result, getString(R.string.download_path_audio_key), DownloadManager.TAG_AUDIO);
|
|
}
|
|
|
|
private void requestDownloadPickVideoFolderResult(final ActivityResult result) {
|
|
requestDownloadPickFolderResult(
|
|
result, getString(R.string.download_path_video_key), DownloadManager.TAG_VIDEO);
|
|
}
|
|
|
|
private void requestDownloadSaveAsResult(@NonNull final ActivityResult result) {
|
|
if (result.getResultCode() != Activity.RESULT_OK) {
|
|
return;
|
|
}
|
|
|
|
if (result.getData() == null || result.getData().getData() == null) {
|
|
showFailedDialog(R.string.general_error);
|
|
return;
|
|
}
|
|
|
|
if (FilePickerActivityHelper.isOwnFileUri(context, result.getData().getData())) {
|
|
final File file = Utils.getFileForUri(result.getData().getData());
|
|
checkSelectedDownload(null, Uri.fromFile(file), file.getName(),
|
|
StoredFileHelper.DEFAULT_MIME);
|
|
return;
|
|
}
|
|
|
|
final DocumentFile docFile = DocumentFile.fromSingleUri(context,
|
|
result.getData().getData());
|
|
if (docFile == null) {
|
|
showFailedDialog(R.string.general_error);
|
|
return;
|
|
}
|
|
|
|
// check if the selected file was previously used
|
|
checkSelectedDownload(null, result.getData().getData(), docFile.getName(),
|
|
docFile.getType());
|
|
}
|
|
|
|
private void requestDownloadPickFolderResult(@NonNull final ActivityResult result,
|
|
final String key,
|
|
final String tag) {
|
|
if (result.getResultCode() != Activity.RESULT_OK) {
|
|
return;
|
|
}
|
|
|
|
if (result.getData() == null || result.getData().getData() == null) {
|
|
showFailedDialog(R.string.general_error);
|
|
return;
|
|
}
|
|
|
|
Uri uri = result.getData().getData();
|
|
if (FilePickerActivityHelper.isOwnFileUri(context, uri)) {
|
|
uri = Uri.fromFile(Utils.getFileForUri(uri));
|
|
} else {
|
|
context.grantUriPermission(context.getPackageName(), uri,
|
|
StoredDirectoryHelper.PERMISSION_FLAGS);
|
|
}
|
|
|
|
PreferenceManager.getDefaultSharedPreferences(context).edit().putString(key,
|
|
uri.toString()).apply();
|
|
|
|
try {
|
|
final StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(context, uri, tag);
|
|
checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp),
|
|
filenameTmp, mimeTmp);
|
|
} catch (final IOException e) {
|
|
showFailedDialog(R.string.general_error);
|
|
}
|
|
}
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Listeners
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
@Override
|
|
public void onCheckedChanged(final RadioGroup group, @IdRes final int checkedId) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "onCheckedChanged() called with: "
|
|
+ "group = [" + group + "], checkedId = [" + checkedId + "]");
|
|
}
|
|
boolean flag = true;
|
|
|
|
switch (checkedId) {
|
|
case R.id.audio_button:
|
|
setupAudioSpinner();
|
|
break;
|
|
case R.id.video_button:
|
|
setupVideoSpinner();
|
|
break;
|
|
case R.id.subtitle_button:
|
|
setupSubtitleSpinner();
|
|
flag = false;
|
|
break;
|
|
}
|
|
|
|
dialogBinding.threads.setEnabled(flag);
|
|
}
|
|
|
|
@Override
|
|
public void onItemSelected(final AdapterView<?> parent,
|
|
final View view,
|
|
final int position,
|
|
final long id) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "onItemSelected() called with: "
|
|
+ "parent = [" + parent + "], view = [" + view + "], "
|
|
+ "position = [" + position + "], id = [" + id + "]");
|
|
}
|
|
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
|
|
case R.id.audio_button:
|
|
selectedAudioIndex = position;
|
|
break;
|
|
case R.id.video_button:
|
|
selectedVideoIndex = position;
|
|
break;
|
|
case R.id.subtitle_button:
|
|
selectedSubtitleIndex = position;
|
|
break;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onNothingSelected(final AdapterView<?> parent) {
|
|
}
|
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
// Download
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
protected void setupDownloadOptions() {
|
|
setRadioButtonsState(false);
|
|
|
|
final boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0;
|
|
final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0;
|
|
final boolean isSubtitleStreamsAvailable = subtitleStreamsAdapter.getCount() > 0;
|
|
|
|
dialogBinding.audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE
|
|
: View.GONE);
|
|
dialogBinding.videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE
|
|
: View.GONE);
|
|
dialogBinding.subtitleButton.setVisibility(isSubtitleStreamsAvailable
|
|
? View.VISIBLE : View.GONE);
|
|
|
|
prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
|
|
final String defaultMedia = prefs.getString(getString(R.string.last_used_download_type),
|
|
getString(R.string.last_download_type_video_key));
|
|
|
|
if (isVideoStreamsAvailable
|
|
&& (defaultMedia.equals(getString(R.string.last_download_type_video_key)))) {
|
|
dialogBinding.videoButton.setChecked(true);
|
|
setupVideoSpinner();
|
|
} else if (isAudioStreamsAvailable
|
|
&& (defaultMedia.equals(getString(R.string.last_download_type_audio_key)))) {
|
|
dialogBinding.audioButton.setChecked(true);
|
|
setupAudioSpinner();
|
|
} else if (isSubtitleStreamsAvailable
|
|
&& (defaultMedia.equals(getString(R.string.last_download_type_subtitle_key)))) {
|
|
dialogBinding.subtitleButton.setChecked(true);
|
|
setupSubtitleSpinner();
|
|
} else if (isVideoStreamsAvailable) {
|
|
dialogBinding.videoButton.setChecked(true);
|
|
setupVideoSpinner();
|
|
} else if (isAudioStreamsAvailable) {
|
|
dialogBinding.audioButton.setChecked(true);
|
|
setupAudioSpinner();
|
|
} else if (isSubtitleStreamsAvailable) {
|
|
dialogBinding.subtitleButton.setChecked(true);
|
|
setupSubtitleSpinner();
|
|
} else {
|
|
Toast.makeText(getContext(), R.string.no_streams_available_download,
|
|
Toast.LENGTH_SHORT).show();
|
|
dismiss();
|
|
}
|
|
}
|
|
|
|
private void setRadioButtonsState(final boolean enabled) {
|
|
dialogBinding.audioButton.setEnabled(enabled);
|
|
dialogBinding.videoButton.setEnabled(enabled);
|
|
dialogBinding.subtitleButton.setEnabled(enabled);
|
|
}
|
|
|
|
private int getSubtitleIndexBy(@NonNull final List<SubtitlesStream> streams) {
|
|
final Localization preferredLocalization = NewPipe.getPreferredLocalization();
|
|
|
|
int candidate = 0;
|
|
for (int i = 0; i < streams.size(); i++) {
|
|
final Locale streamLocale = streams.get(i).getLocale();
|
|
|
|
final boolean languageEquals = streamLocale.getLanguage() != null
|
|
&& preferredLocalization.getLanguageCode() != null
|
|
&& streamLocale.getLanguage()
|
|
.equals(new Locale(preferredLocalization.getLanguageCode()).getLanguage());
|
|
final boolean countryEquals = streamLocale.getCountry() != null
|
|
&& streamLocale.getCountry().equals(preferredLocalization.getCountryCode());
|
|
|
|
if (languageEquals) {
|
|
if (countryEquals) {
|
|
return i;
|
|
}
|
|
|
|
candidate = i;
|
|
}
|
|
}
|
|
|
|
return candidate;
|
|
}
|
|
|
|
@NonNull
|
|
private String getNameEditText() {
|
|
final String str = Objects.requireNonNull(dialogBinding.fileName.getText()).toString()
|
|
.trim();
|
|
|
|
return FilenameUtils.createFilename(context, str.isEmpty() ? currentInfo.getName() : str);
|
|
}
|
|
|
|
private void showFailedDialog(@StringRes final int msg) {
|
|
assureCorrectAppLanguage(getContext());
|
|
new AlertDialog.Builder(context)
|
|
.setTitle(R.string.general_error)
|
|
.setMessage(msg)
|
|
.setNegativeButton(getString(R.string.ok), null)
|
|
.create()
|
|
.show();
|
|
}
|
|
|
|
private void launchDirectoryPicker(final ActivityResultLauncher<Intent> launcher) {
|
|
NoFileManagerSafeGuard.launchSafe(launcher, StoredDirectoryHelper.getPicker(context), TAG,
|
|
context);
|
|
}
|
|
|
|
private void prepareSelectedDownload() {
|
|
final StoredDirectoryHelper mainStorage;
|
|
final MediaFormat format;
|
|
final String selectedMediaType;
|
|
|
|
// first, build the filename and get the output folder (if possible)
|
|
// later, run a very very very large file checking logic
|
|
|
|
filenameTmp = getNameEditText().concat(".");
|
|
|
|
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
|
|
case R.id.audio_button:
|
|
selectedMediaType = getString(R.string.last_download_type_audio_key);
|
|
mainStorage = mainStorageAudio;
|
|
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
|
|
if (format == MediaFormat.WEBMA_OPUS) {
|
|
mimeTmp = "audio/ogg";
|
|
filenameTmp += "opus";
|
|
} else {
|
|
if (format != null) {
|
|
mimeTmp = format.mimeType;
|
|
}
|
|
if (format != null) {
|
|
filenameTmp += format.suffix;
|
|
}
|
|
}
|
|
break;
|
|
case R.id.video_button:
|
|
selectedMediaType = getString(R.string.last_download_type_video_key);
|
|
mainStorage = mainStorageVideo;
|
|
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
|
|
if (format != null) {
|
|
mimeTmp = format.mimeType;
|
|
}
|
|
if (format != null) {
|
|
filenameTmp += format.suffix;
|
|
}
|
|
break;
|
|
case R.id.subtitle_button:
|
|
selectedMediaType = getString(R.string.last_download_type_subtitle_key);
|
|
mainStorage = mainStorageVideo; // subtitle & video files go together
|
|
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
|
|
if (format != null) {
|
|
mimeTmp = format.mimeType;
|
|
}
|
|
|
|
if (format == MediaFormat.TTML) {
|
|
filenameTmp += MediaFormat.SRT.suffix;
|
|
} else {
|
|
if (format != null) {
|
|
filenameTmp += format.suffix;
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
throw new RuntimeException("No stream selected");
|
|
}
|
|
|
|
if (!askForSavePath && (mainStorage == null
|
|
|| mainStorage.isDirect() == NewPipeSettings.useStorageAccessFramework(context)
|
|
|| mainStorage.isInvalidSafStorage())) {
|
|
// Pick new download folder if one of:
|
|
// - Download folder is not set
|
|
// - Download folder uses SAF while SAF is disabled
|
|
// - Download folder doesn't use SAF while SAF is enabled
|
|
// - Download folder uses SAF but the user manually revoked access to it
|
|
Toast.makeText(context, getString(R.string.no_dir_yet),
|
|
Toast.LENGTH_LONG).show();
|
|
|
|
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
|
|
launchDirectoryPicker(requestDownloadPickAudioFolderLauncher);
|
|
} else {
|
|
launchDirectoryPicker(requestDownloadPickVideoFolderLauncher);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (askForSavePath) {
|
|
final Uri initialPath;
|
|
if (NewPipeSettings.useStorageAccessFramework(context)) {
|
|
initialPath = null;
|
|
} else {
|
|
final File initialSavePath;
|
|
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
|
|
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MUSIC);
|
|
} else {
|
|
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES);
|
|
}
|
|
initialPath = Uri.parse(initialSavePath.getAbsolutePath());
|
|
}
|
|
|
|
NoFileManagerSafeGuard.launchSafe(requestDownloadSaveAsLauncher,
|
|
StoredFileHelper.getNewPicker(context, filenameTmp, mimeTmp, initialPath), TAG,
|
|
context);
|
|
|
|
return;
|
|
}
|
|
|
|
// check for existing file with the same name
|
|
checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp), filenameTmp,
|
|
mimeTmp);
|
|
|
|
// remember the last media type downloaded by the user
|
|
prefs.edit().putString(getString(R.string.last_used_download_type), selectedMediaType)
|
|
.apply();
|
|
}
|
|
|
|
private void checkSelectedDownload(final StoredDirectoryHelper mainStorage,
|
|
final Uri targetFile,
|
|
final String filename,
|
|
final String mime) {
|
|
StoredFileHelper storage;
|
|
|
|
try {
|
|
if (mainStorage == null) {
|
|
// using SAF on older android version
|
|
storage = new StoredFileHelper(context, null, targetFile, "");
|
|
} else if (targetFile == null) {
|
|
// the file does not exist, but it is probably used in a pending download
|
|
storage = new StoredFileHelper(mainStorage.getUri(), filename, mime,
|
|
mainStorage.getTag());
|
|
} else {
|
|
// the target filename is already use, attempt to use it
|
|
storage = new StoredFileHelper(context, mainStorage.getUri(), targetFile,
|
|
mainStorage.getTag());
|
|
}
|
|
} catch (final Exception e) {
|
|
ErrorUtil.createNotification(requireContext(),
|
|
new ErrorInfo(e, UserAction.DOWNLOAD_FAILED, "Getting storage"));
|
|
return;
|
|
}
|
|
|
|
// get state of potential mission referring to the same file
|
|
final MissionState state = downloadManager.checkForExistingMission(storage);
|
|
@StringRes final int msgBtn;
|
|
@StringRes final int msgBody;
|
|
|
|
// this switch checks if there is already a mission referring to the same file
|
|
switch (state) {
|
|
case Finished: // there is already a finished mission
|
|
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: // there is no mission referring to the same file
|
|
if (mainStorage == null) {
|
|
// This part is called if:
|
|
// * using SAF on older android version
|
|
// * save path not defined
|
|
// * if the file exists overwrite it, is not necessary ask
|
|
if (!storage.existsAsFile() && !storage.create()) {
|
|
showFailedDialog(R.string.error_file_creation);
|
|
return;
|
|
}
|
|
continueSelectedDownload(storage);
|
|
return;
|
|
} else if (targetFile == null) {
|
|
// This part is called if:
|
|
// * the filename is not used in a pending/finished download
|
|
// * the file does not exists, create
|
|
|
|
if (!mainStorage.mkdirs()) {
|
|
showFailedDialog(R.string.error_path_creation);
|
|
return;
|
|
}
|
|
|
|
storage = mainStorage.createFile(filename, mime);
|
|
if (storage == null || !storage.canWrite()) {
|
|
showFailedDialog(R.string.error_file_creation);
|
|
return;
|
|
}
|
|
|
|
continueSelectedDownload(storage);
|
|
return;
|
|
}
|
|
msgBtn = R.string.overwrite;
|
|
msgBody = R.string.overwrite_unrelated_warning;
|
|
break;
|
|
default:
|
|
return; // unreachable
|
|
}
|
|
|
|
final AlertDialog.Builder askDialog = new AlertDialog.Builder(context)
|
|
.setTitle(R.string.download_dialog_title)
|
|
.setMessage(msgBody)
|
|
.setNegativeButton(R.string.cancel, null);
|
|
final StoredFileHelper finalStorage = storage;
|
|
|
|
|
|
if (mainStorage == null) {
|
|
// This part is called if:
|
|
// * using SAF on older android version
|
|
// * save path not defined
|
|
switch (state) {
|
|
case Pending:
|
|
case Finished:
|
|
askDialog.setPositiveButton(msgBtn, (dialog, which) -> {
|
|
dialog.dismiss();
|
|
downloadManager.forgetMission(finalStorage);
|
|
continueSelectedDownload(finalStorage);
|
|
});
|
|
break;
|
|
}
|
|
|
|
askDialog.create().show();
|
|
return;
|
|
}
|
|
|
|
askDialog.setPositiveButton(msgBtn, (dialog, which) -> {
|
|
dialog.dismiss();
|
|
|
|
StoredFileHelper storageNew;
|
|
switch (state) {
|
|
case Finished:
|
|
case Pending:
|
|
downloadManager.forgetMission(finalStorage);
|
|
case None:
|
|
if (targetFile == null) {
|
|
storageNew = mainStorage.createFile(filename, mime);
|
|
} else {
|
|
try {
|
|
// try take (or steal) the file
|
|
storageNew = new StoredFileHelper(context, mainStorage.getUri(),
|
|
targetFile, mainStorage.getTag());
|
|
} catch (final IOException e) {
|
|
Log.e(TAG, "Failed to take (or steal) the file in "
|
|
+ targetFile.toString());
|
|
storageNew = null;
|
|
}
|
|
}
|
|
|
|
if (storageNew != null && storageNew.canWrite()) {
|
|
continueSelectedDownload(storageNew);
|
|
} else {
|
|
showFailedDialog(R.string.error_file_creation);
|
|
}
|
|
break;
|
|
case PendingRunning:
|
|
storageNew = mainStorage.createUniqueFile(filename, mime);
|
|
if (storageNew == null) {
|
|
showFailedDialog(R.string.error_file_creation);
|
|
} else {
|
|
continueSelectedDownload(storageNew);
|
|
}
|
|
break;
|
|
}
|
|
});
|
|
|
|
askDialog.create().show();
|
|
}
|
|
|
|
private void continueSelectedDownload(@NonNull final StoredFileHelper storage) {
|
|
if (!storage.canWrite()) {
|
|
showFailedDialog(R.string.permission_denied);
|
|
return;
|
|
}
|
|
|
|
// check if the selected file has to be overwritten, by simply checking its length
|
|
try {
|
|
if (storage.length() > 0) {
|
|
storage.truncate();
|
|
}
|
|
} catch (final IOException e) {
|
|
Log.e(TAG, "Failed to truncate the file: " + storage.getUri().toString(), e);
|
|
showFailedDialog(R.string.overwrite_failed);
|
|
return;
|
|
}
|
|
|
|
final Stream selectedStream;
|
|
Stream secondaryStream = null;
|
|
final char kind;
|
|
int threads = dialogBinding.threads.getProgress() + 1;
|
|
final String[] urls;
|
|
final MissionRecoveryInfo[] recoveryInfo;
|
|
String psName = null;
|
|
String[] psArgs = null;
|
|
long nearLength = 0;
|
|
|
|
// more download logic: select muxer, subtitle converter, etc.
|
|
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
|
|
case R.id.audio_button:
|
|
kind = 'a';
|
|
selectedStream = audioStreamsAdapter.getItem(selectedAudioIndex);
|
|
|
|
if (selectedStream.getFormat() == MediaFormat.M4A) {
|
|
psName = Postprocessing.ALGORITHM_M4A_NO_DASH;
|
|
} else if (selectedStream.getFormat() == MediaFormat.WEBMA_OPUS) {
|
|
psName = Postprocessing.ALGORITHM_OGG_FROM_WEBM_DEMUXER;
|
|
}
|
|
break;
|
|
case R.id.video_button:
|
|
kind = 'v';
|
|
selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex);
|
|
|
|
final SecondaryStreamHelper<AudioStream> secondary = videoStreamsAdapter
|
|
.getAllSecondary()
|
|
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
|
|
|
|
if (secondary != null) {
|
|
secondaryStream = secondary.getStream();
|
|
|
|
if (selectedStream.getFormat() == MediaFormat.MPEG_4) {
|
|
psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER;
|
|
} else {
|
|
psName = Postprocessing.ALGORITHM_WEBM_MUXER;
|
|
}
|
|
|
|
psArgs = null;
|
|
final long videoSize = wrappedVideoStreams.getSizeInBytes(
|
|
(VideoStream) selectedStream);
|
|
|
|
// set nearLength, only, if both sizes are fetched or known. This probably
|
|
// does not work on slow networks but is later updated in the downloader
|
|
if (secondary.getSizeInBytes() > 0 && videoSize > 0) {
|
|
nearLength = secondary.getSizeInBytes() + videoSize;
|
|
}
|
|
}
|
|
break;
|
|
case R.id.subtitle_button:
|
|
threads = 1; // use unique thread for subtitles due small file size
|
|
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
|
|
};
|
|
}
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
if (secondaryStream == null) {
|
|
urls = new String[] {
|
|
selectedStream.getContent()
|
|
};
|
|
recoveryInfo = new MissionRecoveryInfo[] {
|
|
new MissionRecoveryInfo(selectedStream)
|
|
};
|
|
} else {
|
|
if (secondaryStream.getDeliveryMethod() != DeliveryMethod.PROGRESSIVE_HTTP) {
|
|
throw new IllegalArgumentException("Unsupported stream delivery format"
|
|
+ secondaryStream.getDeliveryMethod());
|
|
}
|
|
|
|
urls = new String[] {
|
|
selectedStream.getContent(), secondaryStream.getContent()
|
|
};
|
|
recoveryInfo = new MissionRecoveryInfo[] {new MissionRecoveryInfo(selectedStream),
|
|
new MissionRecoveryInfo(secondaryStream)};
|
|
}
|
|
|
|
DownloadManagerService.startMission(context, urls, storage, kind, threads,
|
|
currentInfo.getUrl(), psName, psArgs, nearLength, recoveryInfo);
|
|
|
|
Toast.makeText(context, getString(R.string.download_has_started),
|
|
Toast.LENGTH_SHORT).show();
|
|
|
|
dismiss();
|
|
}
|
|
}
|