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/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 8285a445e..dd067b9b6 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -22,11 +22,13 @@ package org.schabi.newpipe; import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.preference.PreferenceManager; +import android.support.annotation.NonNull; import android.support.design.widget.NavigationView; import android.support.v4.app.Fragment; import android.support.v4.view.GravityCompat; @@ -54,6 +56,7 @@ import org.schabi.newpipe.fragments.list.search.SearchFragment; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.ThemeHelper; @@ -94,10 +97,9 @@ public class MainActivity extends AppCompatActivity { drawerItems = findViewById(R.id.navigation); for(StreamingService s : NewPipe.getServices()) { - String title = - s.getServiceInfo().getName() + - (ServiceHelper.isBeta(s) ? " (beta)" : ""); - MenuItem item = drawerItems.getMenu() + final String title = s.getServiceInfo().getName() + + (ServiceHelper.isBeta(s) ? " (beta)" : ""); + final MenuItem item = drawerItems.getMenu() .add(R.id.menu_services_group, s.getServiceId(), 0, title); item.setIcon(ServiceHelper.getIcon(s.getServiceId())); } @@ -233,6 +235,26 @@ public class MainActivity extends AppCompatActivity { } else super.onBackPressed(); } + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + for (int i: grantResults){ + if (i == PackageManager.PERMISSION_DENIED){ + return; + } + } + switch (requestCode) { + case PermissionHelper.DOWNLOADS_REQUEST_CODE: + NavigationHelper.openDownloads(this); + break; + case PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE: + Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder); + if (fragment instanceof VideoDetailFragment) { + ((VideoDetailFragment) fragment).openDownloadDialog(); + } + break; + } + } + /** * Implement the following diagram behavior for the up button: *
@@ -313,6 +335,9 @@ public class MainActivity extends AppCompatActivity {
case R.id.action_about:
NavigationHelper.openAbout(this);
return true;
+ case R.id.action_history:
+ NavigationHelper.openHistory(this);
+ return true;
default:
return super.onOptionsItemSelected(item);
}
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..75f05cd16 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;
}
/*//////////////////////////////////////////////////////////////////////////
@@ -79,33 +117,26 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
- if (!PermissionHelper.checkStoragePermissions(getActivity())) {
+ if (!PermissionHelper.checkStoragePermissions(getActivity(), PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
getDialog().dismiss();
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..e3b826feb 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
@@ -355,21 +354,8 @@ public class VideoDetailFragment
}
break;
case R.id.detail_controls_download:
- if (!PermissionHelper.checkStoragePermissions(activity)) {
- return;
- }
-
- try {
- DownloadDialog downloadDialog =
- DownloadDialog.newInstance(currentInfo,
- sortedStreamVideosList,
- selectedVideoStream);
- downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
- } catch (Exception e) {
- Toast.makeText(activity,
- R.string.could_not_setup_download_menu,
- Toast.LENGTH_LONG).show();
- e.printStackTrace();
+ if (PermissionHelper.checkStoragePermissions(activity, PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
+ this.openDownloadDialog();
}
break;
case R.id.detail_uploader_root_layout:
@@ -411,6 +397,9 @@ public class VideoDetailFragment
case R.id.detail_controls_popup:
openPopupPlayer(true);
break;
+ case R.id.detail_controls_download:
+ NavigationHelper.openDownloads(getActivity());
+ break;
}
return true;
@@ -532,6 +521,7 @@ public class VideoDetailFragment
detailControlsPopup.setOnClickListener(this);
detailControlsAddToPlaylist.setOnClickListener(this);
detailControlsDownload.setOnClickListener(this);
+ detailControlsDownload.setOnLongClickListener(this);
relatedStreamExpandButton.setOnClickListener(this);
detailControlsBackground.setLongClickable(true);
@@ -721,27 +711,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 +941,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) {
@@ -1227,6 +1216,23 @@ public class VideoDetailFragment
}
}
+
+ public void openDownloadDialog() {
+ try {
+ 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,
+ R.string.could_not_setup_download_menu,
+ Toast.LENGTH_LONG).show();
+ e.printStackTrace();
+ }
+ }
+
/*//////////////////////////////////////////////////////////////////////////
// Stream Results
//////////////////////////////////////////////////////////////////////////*/
diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
index 3d9a3e0de..7454aaf57 100644
--- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
@@ -428,7 +428,7 @@ public class NavigationHelper {
}
public static boolean openDownloads(Activity activity) {
- if (!PermissionHelper.checkStoragePermissions(activity)) {
+ if (!PermissionHelper.checkStoragePermissions(activity, PermissionHelper.DOWNLOADS_REQUEST_CODE)) {
return false;
}
Intent intent = new Intent(activity, DownloadActivity.class);
diff --git a/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java b/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java
index a33348934..7574a9304 100644
--- a/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java
@@ -18,26 +18,26 @@ import android.widget.Toast;
import org.schabi.newpipe.R;
public class PermissionHelper {
- public static final int PERMISSION_WRITE_STORAGE = 778;
- public static final int PERMISSION_READ_STORAGE = 777;
+ public static final int DOWNLOAD_DIALOG_REQUEST_CODE = 778;
+ public static final int DOWNLOADS_REQUEST_CODE = 777;
- public static boolean checkStoragePermissions(Activity activity) {
+ public static boolean checkStoragePermissions(Activity activity, int requestCode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
- if(!checkReadStoragePermissions(activity)) return false;
+ if(!checkReadStoragePermissions(activity, requestCode)) return false;
}
- return checkWriteStoragePermissions(activity);
+ return checkWriteStoragePermissions(activity, requestCode);
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
- public static boolean checkReadStoragePermissions(Activity activity) {
+ public static boolean checkReadStoragePermissions(Activity activity, int requestCode) {
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(activity,
new String[]{
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE},
- PERMISSION_READ_STORAGE);
+ requestCode);
return false;
}
@@ -45,7 +45,7 @@ public class PermissionHelper {
}
- public static boolean checkWriteStoragePermissions(Activity activity) {
+ public static boolean checkWriteStoragePermissions(Activity activity, int requestCode) {
// Here, thisActivity is the current activity
if (ContextCompat.checkSelfPermission(activity,
Manifest.permission.WRITE_EXTERNAL_STORAGE)
@@ -63,7 +63,7 @@ public class PermissionHelper {
// No explanation needed, we can request the permission.
ActivityCompat.requestPermissions(activity,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
- PERMISSION_WRITE_STORAGE);
+ requestCode);
// PERMISSION_WRITE_STORAGE is an
// app-defined int constant. The callback method gets the
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/menu/main_menu.xml b/app/src/main/res/menu/main_menu.xml
index df22b47c0..b6372433e 100644
--- a/app/src/main/res/menu/main_menu.xml
+++ b/app/src/main/res/menu/main_menu.xml
@@ -1,8 +1,27 @@
-