From 35b3bf2edb62c1bafd0cbdbe36915009ccb48521 Mon Sep 17 00:00:00 2001 From: Mauricio Colli Date: Sun, 8 Apr 2018 08:08:19 -0300 Subject: [PATCH] Show download size preview --- .../java/org/schabi/newpipe/Downloader.java | 25 ++ .../newpipe/download/DownloadDialog.java | 215 +++++++++++------- .../detail/SpinnerToolbarAdapter.java | 74 ------ .../fragments/detail/VideoDetailFragment.java | 49 ++-- .../newpipe/util/StreamItemAdapter.java | 196 ++++++++++++++++ .../{dialog_url.xml => download_dialog.xml} | 10 +- .../res/layout/resolutions_spinner_item.xml | 34 --- .../main/res/layout/stream_quality_item.xml | 66 ++++++ app/src/main/res/values/strings.xml | 1 + 9 files changed, 454 insertions(+), 216 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/fragments/detail/SpinnerToolbarAdapter.java create mode 100644 app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java rename app/src/main/res/layout/{dialog_url.xml => download_dialog.xml} (96%) delete mode 100644 app/src/main/res/layout/resolutions_spinner_item.xml create mode 100644 app/src/main/res/layout/stream_quality_item.xml diff --git a/app/src/main/java/org/schabi/newpipe/Downloader.java b/app/src/main/java/org/schabi/newpipe/Downloader.java index d9537c6b6..17dc5859d 100644 --- a/app/src/main/java/org/schabi/newpipe/Downloader.java +++ b/app/src/main/java/org/schabi/newpipe/Downloader.java @@ -73,6 +73,31 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader { mCookies = cookies; } + /** + * Get the size of the content that the url is pointing by firing a HEAD request. + * + * @param url an url pointing to the content + * @return the size of the content, in bytes + */ + public long getContentLength(String url) throws IOException { + Response response = null; + try { + final Request request = new Request.Builder() + .head().url(url) + .addHeader("User-Agent", USER_AGENT) + .build(); + response = client.newCall(request).execute(); + + return Long.parseLong(response.header("Content-Length")); + } catch (NumberFormatException e) { + throw new IOException("Invalid content length", e); + } finally { + if (response != null) { + response.close(); + } + } + } + /** * Download the text file at the supplied URL as in download(String), * but set the HTTP header field "Accept-Language" to the supplied string. diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index 9bcd0bcb7..ee278a628 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -1,56 +1,61 @@ package org.schabi.newpipe.download; +import android.content.Context; import android.os.Bundle; import android.support.annotation.IdRes; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.DialogFragment; import android.support.v7.widget.Toolbar; import android.util.Log; import android.view.LayoutInflater; -import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; -import android.widget.ArrayAdapter; import android.widget.EditText; import android.widget.RadioButton; import android.widget.RadioGroup; import android.widget.SeekBar; import android.widget.Spinner; import android.widget.TextView; +import android.widget.Toast; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.stream.AudioStream; +import org.schabi.newpipe.extractor.stream.Stream; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.VideoStream; -import org.schabi.newpipe.fragments.detail.SpinnerToolbarAdapter; import org.schabi.newpipe.settings.NewPipeSettings; import org.schabi.newpipe.util.FilenameUtils; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.PermissionHelper; +import org.schabi.newpipe.util.StreamItemAdapter; +import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper; import org.schabi.newpipe.util.ThemeHelper; -import java.io.Serializable; import java.util.ArrayList; import java.util.List; +import icepick.Icepick; +import icepick.State; +import io.reactivex.disposables.CompositeDisposable; import us.shandian.giga.service.DownloadManagerService; public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener { private static final String TAG = "DialogFragment"; private static final boolean DEBUG = MainActivity.DEBUG; - private static final String INFO_KEY = "info_key"; - private static final String SORTED_VIDEOS_LIST_KEY = "sorted_videos_list_key"; - private static final String SELECTED_VIDEO_KEY = "selected_video_key"; - private static final String SELECTED_AUDIO_KEY = "selected_audio_key"; + @State protected StreamInfo currentInfo; + @State protected StreamSizeWrapper wrappedAudioStreams = StreamSizeWrapper.empty(); + @State protected StreamSizeWrapper wrappedVideoStreams = StreamSizeWrapper.empty(); + @State protected int selectedVideoIndex = 0; + @State protected int selectedAudioIndex = 0; - private StreamInfo currentInfo; - private ArrayList sortedStreamVideosList; - private int selectedVideoIndex; - private int selectedAudioIndex; + private StreamItemAdapter audioStreamsAdapter; + private StreamItemAdapter videoStreamsAdapter; + + private CompositeDisposable disposables = new CompositeDisposable(); private EditText nameEditText; private Spinner streamsSpinner; @@ -58,17 +63,50 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck private TextView threadsCountTextView; private SeekBar threadsSeekBar; - public static DownloadDialog newInstance(StreamInfo info, ArrayList sortedStreamVideosList, int selectedVideoIndex) { + public static DownloadDialog newInstance(StreamInfo info) { DownloadDialog dialog = new DownloadDialog(); - dialog.setInfo(info, sortedStreamVideosList, selectedVideoIndex); - dialog.setStyle(DialogFragment.STYLE_NO_TITLE, 0); + dialog.setInfo(info); return dialog; } - private void setInfo(StreamInfo info, ArrayList sortedStreamVideosList, int selectedVideoIndex) { + public static DownloadDialog newInstance(Context context, StreamInfo info) { + final ArrayList streamsList = new ArrayList<>(ListHelper.getSortedStreamVideosList(context, + info.getVideoStreams(), info.getVideoOnlyStreams(), false)); + final int selectedStreamIndex = ListHelper.getDefaultResolutionIndex(context, streamsList); + + final DownloadDialog instance = newInstance(info); + instance.setVideoStreams(streamsList); + instance.setSelectedVideoStream(selectedStreamIndex); + instance.setAudioStreams(info.getAudioStreams()); + return instance; + } + + private void setInfo(StreamInfo info) { this.currentInfo = info; + } + + public void setAudioStreams(List audioStreams) { + setAudioStreams(new StreamSizeWrapper<>(audioStreams)); + } + + public void setAudioStreams(StreamSizeWrapper wrappedAudioStreams) { + this.wrappedAudioStreams = wrappedAudioStreams; + } + + public void setVideoStreams(List videoStreams) { + setVideoStreams(new StreamSizeWrapper<>(videoStreams)); + } + + public void setVideoStreams(StreamSizeWrapper wrappedVideoStreams) { + this.wrappedVideoStreams = wrappedVideoStreams; + } + + public void setSelectedVideoStream(int selectedVideoIndex) { this.selectedVideoIndex = selectedVideoIndex; - this.sortedStreamVideosList = sortedStreamVideosList; + } + + public void setSelectedAudioStream(int selectedAudioIndex) { + this.selectedAudioIndex = selectedAudioIndex; } /*////////////////////////////////////////////////////////////////////////// @@ -84,28 +122,21 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck return; } - if (savedInstanceState != null) { - Serializable serial = savedInstanceState.getSerializable(INFO_KEY); - if (serial instanceof StreamInfo) currentInfo = (StreamInfo) serial; + setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(getContext())); + Icepick.restoreInstanceState(this, savedInstanceState); - serial = savedInstanceState.getSerializable(SORTED_VIDEOS_LIST_KEY); - if (serial instanceof ArrayList) { //noinspection unchecked - sortedStreamVideosList = (ArrayList) serial; - } - - selectedVideoIndex = savedInstanceState.getInt(SELECTED_VIDEO_KEY, 0); - selectedAudioIndex = savedInstanceState.getInt(SELECTED_AUDIO_KEY, 0); - } + this.videoStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedVideoStreams, true); + this.audioStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedAudioStreams); } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { if (DEBUG) Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]"); - return inflater.inflate(R.layout.dialog_url, container); + return inflater.inflate(R.layout.download_dialog, container); } @Override - public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); nameEditText = view.findViewById(R.id.file_name); nameEditText.setText(FilenameUtils.createFilename(getContext(), currentInfo.getName())); @@ -116,12 +147,12 @@ 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); - initToolbar(view.findViewById(R.id.toolbar)); - checkDownloadOptions(view); - setupVideoSpinner(sortedStreamVideosList, streamsSpinner); + initToolbar(view.findViewById(R.id.toolbar)); + setupDownloadOptions(); int def = 3; threadsCountTextView.setText(String.valueOf(def)); @@ -141,15 +172,35 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck public void onStopTrackingTouch(SeekBar p1) { } }); + + fetchStreamsSize(); + } + + private void fetchStreamsSize() { + disposables.clear(); + + disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams).subscribe(result -> { + if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.video_button) { + setupVideoSpinner(); + } + })); + disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams).subscribe(result -> { + if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) { + setupAudioSpinner(); + } + })); + } + + @Override + public void onDestroy() { + super.onDestroy(); + disposables.clear(); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); - outState.putSerializable(INFO_KEY, currentInfo); - outState.putSerializable(SORTED_VIDEOS_LIST_KEY, sortedStreamVideosList); - outState.putInt(SELECTED_VIDEO_KEY, selectedVideoIndex); - outState.putInt(SELECTED_AUDIO_KEY, selectedAudioIndex); + Icepick.saveInstanceState(this, outState); } /*////////////////////////////////////////////////////////////////////////// @@ -161,39 +212,31 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck 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.inflateMenu(R.menu.dialog_url); - toolbar.setNavigationOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - getDialog().dismiss(); - } - }); + toolbar.setNavigationOnClickListener(v -> getDialog().dismiss()); - toolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - if (item.getItemId() == R.id.okay) { - downloadSelected(); - return true; - } else return false; + toolbar.setOnMenuItemClickListener(item -> { + if (item.getItemId() == R.id.okay) { + downloadSelected(); + return true; } + return false; }); } - public void setupAudioSpinner(final List audioStreams, Spinner spinner) { - String[] items = new String[audioStreams.size()]; - for (int i = 0; i < audioStreams.size(); i++) { - AudioStream audioStream = audioStreams.get(i); - items[i] = audioStream.getFormat().getName() + " " + audioStream.getAverageBitrate() + "kbps"; - } + private void setupAudioSpinner() { + if (getContext() == null) return; - ArrayAdapter itemAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, items); - spinner.setAdapter(itemAdapter); - spinner.setSelection(selectedAudioIndex); + streamsSpinner.setAdapter(audioStreamsAdapter); + streamsSpinner.setSelection(selectedAudioIndex); + setRadioButtonsState(true); } - public void setupVideoSpinner(final List videoStreams, Spinner spinner) { - spinner.setAdapter(new SpinnerToolbarAdapter(getContext(), videoStreams, true)); - spinner.setSelection(selectedVideoIndex); + private void setupVideoSpinner() { + if (getContext() == null) return; + + streamsSpinner.setAdapter(videoStreamsAdapter); + streamsSpinner.setSelection(selectedVideoIndex); + setRadioButtonsState(true); } /*////////////////////////////////////////////////////////////////////////// @@ -205,10 +248,10 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck if (DEBUG) Log.d(TAG, "onCheckedChanged() called with: group = [" + group + "], checkedId = [" + checkedId + "]"); switch (checkedId) { case R.id.audio_button: - setupAudioSpinner(currentInfo.getAudioStreams(), streamsSpinner); + setupAudioSpinner(); break; case R.id.video_button: - setupVideoSpinner(sortedStreamVideosList, streamsSpinner); + setupVideoSpinner(); break; } } @@ -238,37 +281,53 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck // Utils //////////////////////////////////////////////////////////////////////////*/ - protected void checkDownloadOptions(View view) { - RadioButton audioButton = view.findViewById(R.id.audio_button); - RadioButton videoButton = view.findViewById(R.id.video_button); + protected void setupDownloadOptions() { + setRadioButtonsState(false); - if (currentInfo.getAudioStreams() == null || currentInfo.getAudioStreams().size() == 0) { - audioButton.setVisibility(View.GONE); + final RadioButton audioButton = radioVideoAudioGroup.findViewById(R.id.audio_button); + final RadioButton videoButton = radioVideoAudioGroup.findViewById(R.id.video_button); + final boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0; + final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0; + + audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE : View.GONE); + videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE : View.GONE); + + if (isVideoStreamsAvailable) { videoButton.setChecked(true); - } else if (sortedStreamVideosList == null || sortedStreamVideosList.size() == 0) { - videoButton.setVisibility(View.GONE); + setupVideoSpinner(); + } else if (isAudioStreamsAvailable) { audioButton.setChecked(true); + setupAudioSpinner(); + } else { + Toast.makeText(getContext(), R.string.no_streams_available_download, Toast.LENGTH_SHORT).show(); + getDialog().dismiss(); } } + private void setRadioButtonsState(boolean enabled) { + radioVideoAudioGroup.findViewById(R.id.audio_button).setEnabled(enabled); + radioVideoAudioGroup.findViewById(R.id.video_button).setEnabled(enabled); + } private void downloadSelected() { - String url, location; + Stream stream; + String location; String fileName = nameEditText.getText().toString().trim(); if (fileName.isEmpty()) fileName = FilenameUtils.createFilename(getContext(), currentInfo.getName()); boolean isAudio = radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button; if (isAudio) { - url = currentInfo.getAudioStreams().get(selectedAudioIndex).getUrl(); + stream = audioStreamsAdapter.getItem(selectedAudioIndex); location = NewPipeSettings.getAudioDownloadPath(getContext()); - fileName += "." + currentInfo.getAudioStreams().get(selectedAudioIndex).getFormat().getSuffix(); } else { - url = sortedStreamVideosList.get(selectedVideoIndex).getUrl(); + stream = videoStreamsAdapter.getItem(selectedVideoIndex); location = NewPipeSettings.getVideoDownloadPath(getContext()); - fileName += "." + sortedStreamVideosList.get(selectedVideoIndex).getFormat().getSuffix(); } + String url = stream.getUrl(); + fileName += "." + stream.getFormat().getSuffix(); + DownloadManagerService.startMission(getContext(), url, location, fileName, isAudio, threadsSeekBar.getProgress() + 1); getDialog().dismiss(); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/SpinnerToolbarAdapter.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/SpinnerToolbarAdapter.java deleted file mode 100644 index 33f87be70..000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/SpinnerToolbarAdapter.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.schabi.newpipe.fragments.detail; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; -import android.widget.ImageView; -import android.widget.Spinner; -import android.widget.TextView; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.MediaFormat; -import org.schabi.newpipe.extractor.stream.VideoStream; - -import java.util.List; - -public class SpinnerToolbarAdapter extends BaseAdapter { - private final List videoStreams; - private final boolean showIconNoAudio; - - private final Context context; - - public SpinnerToolbarAdapter(Context context, List videoStreams, boolean showIconNoAudio) { - this.context = context; - this.videoStreams = videoStreams; - this.showIconNoAudio = showIconNoAudio; - } - - @Override - public int getCount() { - return videoStreams.size(); - } - - @Override - public Object getItem(int position) { - return videoStreams.get(position); - } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - public View getDropDownView(int position, View convertView, ViewGroup parent) { - return getCustomView(position, convertView, parent, true); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - return getCustomView(((Spinner) parent).getSelectedItemPosition(), convertView, parent, false); - } - - private View getCustomView(int position, View convertView, ViewGroup parent, boolean isDropdownItem) { - if (convertView == null) { - convertView = LayoutInflater.from(context).inflate(R.layout.resolutions_spinner_item, parent, false); - } - - ImageView woSoundIcon = convertView.findViewById(R.id.wo_sound_icon); - TextView text = convertView.findViewById(android.R.id.text1); - VideoStream item = (VideoStream) getItem(position); - text.setText(item.getFormat().getName() + " " + item.getResolution()); - - int visibility = !showIconNoAudio ? View.GONE - : item.isVideoOnly ? View.VISIBLE - : isDropdownItem ? View.INVISIBLE - : View.GONE; - woSoundIcon.setVisibility(visibility); - - return convertView; - } - -} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 611cd8bfb..d1ae3a270 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -12,6 +12,7 @@ import android.preference.PreferenceManager; import android.support.annotation.DrawableRes; import android.support.annotation.FloatRange; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.support.v4.view.animation.FastOutSlowInInterpolator; import android.support.v7.app.ActionBar; @@ -61,6 +62,8 @@ import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.BaseStateFragment; +import org.schabi.newpipe.util.StreamItemAdapter; +import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper; import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog; import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoItemDialog; @@ -83,9 +86,9 @@ import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.ThemeHelper; import java.io.Serializable; -import java.util.ArrayList; import java.util.Collection; import java.util.LinkedList; +import java.util.List; import icepick.State; import io.reactivex.Single; @@ -107,8 +110,6 @@ public class VideoDetailFragment // Amount of videos to show on start private static final int INITIAL_RELATED_VIDEOS = 8; - private ArrayList sortedStreamVideosList; - private InfoItemBuilder infoItemBuilder = null; private int updateFlags = 0; @@ -120,18 +121,16 @@ public class VideoDetailFragment private boolean showRelatedStreams; private boolean wasRelatedStreamsExpanded = false; - @State - protected int serviceId = Constants.NO_SERVICE_ID; - @State - protected String name; - @State - protected String url; + @State protected int serviceId = Constants.NO_SERVICE_ID; + @State protected String name; + @State protected String url; private StreamInfo currentInfo; private Disposable currentWorker; private CompositeDisposable disposables = new CompositeDisposable(); - private int selectedVideoStream = -1; + private List sortedVideoStreams; + private int selectedVideoStreamIndex = -1; /*////////////////////////////////////////////////////////////////////////// // Views @@ -360,10 +359,11 @@ public class VideoDetailFragment } try { - DownloadDialog downloadDialog = - DownloadDialog.newInstance(currentInfo, - sortedStreamVideosList, - selectedVideoStream); + DownloadDialog downloadDialog = DownloadDialog.newInstance(currentInfo); + downloadDialog.setVideoStreams(sortedVideoStreams); + downloadDialog.setAudioStreams(currentInfo.getAudioStreams()); + downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex); + downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog"); } catch (Exception e) { Toast.makeText(activity, @@ -721,27 +721,25 @@ public class VideoDetailFragment private void setupActionBar(final StreamInfo info) { if (DEBUG) Log.d(TAG, "setupActionBarHandler() called with: info = [" + info + "]"); - sortedStreamVideosList = new ArrayList<>(ListHelper.getSortedStreamVideosList( - activity, info.getVideoStreams(), info.getVideoOnlyStreams(), false)); - - selectedVideoStream = ListHelper.getDefaultResolutionIndex(activity, sortedStreamVideosList); - boolean isExternalPlayerEnabled = PreferenceManager.getDefaultSharedPreferences(activity) .getBoolean(activity.getString(R.string.use_external_video_player_key), false); - spinnerToolbar.setAdapter(new SpinnerToolbarAdapter(activity, sortedStreamVideosList, - isExternalPlayerEnabled)); - spinnerToolbar.setSelection(selectedVideoStream); + + sortedVideoStreams = ListHelper.getSortedStreamVideosList(activity, info.getVideoStreams(), info.getVideoOnlyStreams(), false); + selectedVideoStreamIndex = ListHelper.getDefaultResolutionIndex(activity, sortedVideoStreams); + + final StreamItemAdapter streamsAdapter = new StreamItemAdapter<>(activity, new StreamSizeWrapper<>(sortedVideoStreams), isExternalPlayerEnabled); + spinnerToolbar.setAdapter(streamsAdapter); + spinnerToolbar.setSelection(selectedVideoStreamIndex); spinnerToolbar.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { - selectedVideoStream = position; + selectedVideoStreamIndex = position; } @Override public void onNothingSelected(AdapterView parent) { } }); - } /*////////////////////////////////////////////////////////////////////////// @@ -953,8 +951,9 @@ public class VideoDetailFragment this.autoPlayEnabled = autoplay; } + @Nullable private VideoStream getSelectedVideoStream() { - return sortedStreamVideosList.get(selectedVideoStream); + return sortedVideoStreams != null ? sortedVideoStreams.get(selectedVideoStreamIndex) : null; } private void prepareDescription(final String descriptionHtml) { diff --git a/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java b/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java new file mode 100644 index 000000000..e3fe4a679 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java @@ -0,0 +1,196 @@ +package org.schabi.newpipe.util; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import android.widget.Spinner; +import android.widget.TextView; + +import org.schabi.newpipe.Downloader; +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.stream.AudioStream; +import org.schabi.newpipe.extractor.stream.Stream; +import org.schabi.newpipe.extractor.stream.VideoStream; + +import java.io.Serializable; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; + +import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; +import us.shandian.giga.util.Utility; + +/** + * A list adapter for a list of {@link Stream streams}, currently supporting {@link VideoStream} and {@link AudioStream}. + */ +public class StreamItemAdapter extends BaseAdapter { + private final Context context; + + private StreamSizeWrapper streamsWrapper; + private final boolean showIconNoAudio; + + public StreamItemAdapter(Context context, StreamSizeWrapper streamsWrapper, boolean showIconNoAudio) { + this.context = context; + this.streamsWrapper = streamsWrapper; + this.showIconNoAudio = showIconNoAudio; + } + + public StreamItemAdapter(Context context, StreamSizeWrapper streamsWrapper) { + this(context, streamsWrapper, false); + } + + public List getAll() { + return streamsWrapper.getStreamsList(); + } + + @Override + public int getCount() { + return streamsWrapper.getStreamsList().size(); + } + + @Override + public T getItem(int position) { + return streamsWrapper.getStreamsList().get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getDropDownView(int position, View convertView, ViewGroup parent) { + return getCustomView(position, convertView, parent, true); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + return getCustomView(((Spinner) parent).getSelectedItemPosition(), convertView, parent, false); + } + + private View getCustomView(int position, View convertView, ViewGroup parent, boolean isDropdownItem) { + if (convertView == null) { + convertView = LayoutInflater.from(context).inflate(R.layout.stream_quality_item, parent, false); + } + + final ImageView woSoundIconView = convertView.findViewById(R.id.wo_sound_icon); + final TextView formatNameView = convertView.findViewById(R.id.stream_format_name); + final TextView qualityView = convertView.findViewById(R.id.stream_quality); + final TextView sizeView = convertView.findViewById(R.id.stream_size); + + final T stream = getItem(position); + + int woSoundIconVisibility = View.GONE; + String qualityString; + + if (stream instanceof VideoStream) { + qualityString = ((VideoStream) stream).getResolution(); + + if (!showIconNoAudio) { + woSoundIconVisibility = View.GONE; + } else if (((VideoStream) stream).isVideoOnly()) { + woSoundIconVisibility = View.VISIBLE; + } else if (isDropdownItem) { + woSoundIconVisibility = View.INVISIBLE; + } + } else if (stream instanceof AudioStream) { + qualityString = ((AudioStream) stream).getAverageBitrate() + "kbps"; + } else { + qualityString = stream.getFormat().getSuffix(); + } + + if (streamsWrapper.getSizeInBytes(position) > 0) { + sizeView.setText(streamsWrapper.getFormattedSize(position)); + sizeView.setVisibility(View.VISIBLE); + } else { + sizeView.setVisibility(View.GONE); + } + + formatNameView.setText(stream.getFormat().getName()); + qualityView.setText(qualityString); + woSoundIconView.setVisibility(woSoundIconVisibility); + + return convertView; + } + + /** + * A wrapper class that includes a way of storing the stream sizes. + */ + public static class StreamSizeWrapper implements Serializable { + private static final StreamSizeWrapper EMPTY = new StreamSizeWrapper<>(Collections.emptyList()); + private final List streamsList; + private long[] streamSizes; + + public StreamSizeWrapper(List streamsList) { + this.streamsList = streamsList; + this.streamSizes = new long[streamsList.size()]; + + for (int i = 0; i < streamSizes.length; i++) streamSizes[i] = -1; + } + + /** + * Helper method to fetch the sizes of all the streams in a wrapper. + * + * @param streamsWrapper the wrapper + * @return a {@link Single} that returns a boolean indicating if any elements were changed + */ + public static Single fetchSizeForWrapper(StreamSizeWrapper streamsWrapper) { + final Callable fetchAndSet = () -> { + boolean hasChanged = false; + for (X stream : streamsWrapper.getStreamsList()) { + if (streamsWrapper.getSizeInBytes(stream) > 0) { + continue; + } + + final long contentLength = Downloader.getInstance().getContentLength(stream.getUrl()); + streamsWrapper.setSize(stream, contentLength); + hasChanged = true; + } + return hasChanged; + }; + + return Single.fromCallable(fetchAndSet) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .onErrorReturnItem(true); + } + + public List getStreamsList() { + return streamsList; + } + + public long getSizeInBytes(int streamIndex) { + return streamSizes[streamIndex]; + } + + public long getSizeInBytes(T stream) { + return streamSizes[streamsList.indexOf(stream)]; + } + + public String getFormattedSize(int streamIndex) { + return Utility.formatBytes(getSizeInBytes(streamIndex)); + } + + public String getFormattedSize(T stream) { + return Utility.formatBytes(getSizeInBytes(stream)); + } + + public void setSize(int streamIndex, long sizeInBytes) { + streamSizes[streamIndex] = sizeInBytes; + } + + public void setSize(T stream, long sizeInBytes) { + streamSizes[streamsList.indexOf(stream)] = sizeInBytes; + } + + public static StreamSizeWrapper empty() { + //noinspection unchecked + return (StreamSizeWrapper) EMPTY; + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_url.xml b/app/src/main/res/layout/download_dialog.xml similarity index 96% rename from app/src/main/res/layout/dialog_url.xml rename to app/src/main/res/layout/download_dialog.xml index eef44e76b..2cdfee553 100644 --- a/app/src/main/res/layout/dialog_url.xml +++ b/app/src/main/res/layout/download_dialog.xml @@ -35,8 +35,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/file_name" - android:layout_marginLeft="20dp" android:layout_marginBottom="6dp" + android:layout_marginLeft="20dp" android:gravity="left" android:orientation="horizontal" tools:ignore="RtlHardcoded"> @@ -59,12 +59,12 @@ android:id="@+id/quality_spinner" android:layout_width="match_parent" android:layout_height="wrap_content" - android:minWidth="150dp" android:layout_below="@+id/video_audio_group" android:layout_marginBottom="12dp" android:layout_marginLeft="20dp" android:layout_marginRight="20dp" - tools:listitem="@layout/resolutions_spinner_item"/> + android:minWidth="150dp" + tools:listitem="@layout/stream_quality_item"/> + android:orientation="horizontal" + android:paddingBottom="12dp"> - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/stream_quality_item.xml b/app/src/main/res/layout/stream_quality_item.xml new file mode 100644 index 000000000..76f52a2a0 --- /dev/null +++ b/app/src/main/res/layout/stream_quality_item.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2419b8084..f22f42e95 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -180,6 +180,7 @@ File doesn\'t exist or insufficient permission to read or write to it File name cannot be empty An error occurred: %1$s + No streams available to download Sorry, that should not have happened.