From 5ca0a0adf2fd8707a5bdb32b6c48a5956e258902 Mon Sep 17 00:00:00 2001 From: Aris Poloway Date: Fri, 6 Apr 2018 17:19:45 -0400 Subject: [PATCH 1/8] Open downloads after permission granted --- .../java/org/schabi/newpipe/MainActivity.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 8285a445e..4b31a1b1f 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; @@ -233,6 +236,19 @@ public class MainActivity extends AppCompatActivity { } else super.onBackPressed(); } + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (requestCode == PermissionHelper.PERMISSION_READ_STORAGE || + requestCode == PermissionHelper.PERMISSION_WRITE_STORAGE){ + for (int i: grantResults){ + if (i == PackageManager.PERMISSION_DENIED){ + return; + } + } + NavigationHelper.openDownloads(this); + } + } + /** * Implement the following diagram behavior for the up button: *

From 676d64a24a3934cff5cf1464b5f2be49dac34b4f Mon Sep 17 00:00:00 2001
From: Aris Poloway 
Date: Sat, 7 Apr 2018 13:32:02 -0400
Subject: [PATCH 2/8] Open download dialog if video download is clicked instead

---
 .../java/org/schabi/newpipe/MainActivity.java | 21 ++++++++----
 .../newpipe/download/DownloadDialog.java      |  2 +-
 .../fragments/detail/VideoDetailFragment.java | 32 +++++++++++--------
 .../schabi/newpipe/util/NavigationHelper.java |  2 +-
 .../schabi/newpipe/util/PermissionHelper.java | 18 +++++------
 5 files changed, 43 insertions(+), 32 deletions(-)

diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java
index 4b31a1b1f..1fdb67cf3 100644
--- a/app/src/main/java/org/schabi/newpipe/MainActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java
@@ -238,14 +238,21 @@ public class MainActivity extends AppCompatActivity {
 
     @Override
     public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
-        if (requestCode == PermissionHelper.PERMISSION_READ_STORAGE ||
-                requestCode == PermissionHelper.PERMISSION_WRITE_STORAGE){
-            for (int i: grantResults){
-                if (i == PackageManager.PERMISSION_DENIED){
-                    return;
-                }
+        for (int i: grantResults){
+            if (i == PackageManager.PERMISSION_DENIED){
+                return;
             }
-            NavigationHelper.openDownloads(this);
+        }
+        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;
         }
     }
 
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..2934284c5 100644
--- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
@@ -79,7 +79,7 @@ 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;
         }
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 74e561f99..653ce2082 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
@@ -356,22 +356,10 @@ public class VideoDetailFragment
                 }
                 break;
             case R.id.detail_controls_download:
-                if (!PermissionHelper.checkStoragePermissions(activity)) {
+                if (!PermissionHelper.checkStoragePermissions(activity, PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
                     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();
-                }
+                this.openDownloadDialog();
                 break;
             case R.id.detail_uploader_root_layout:
                 if (TextUtils.isEmpty(currentInfo.getUploaderUrl())) {
@@ -1226,6 +1214,22 @@ public class VideoDetailFragment
         }
     }
 
+
+    public void openDownloadDialog() {
+        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();
+        }
+    }
+
     /*//////////////////////////////////////////////////////////////////////////
     // 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 26088a64c..288191677 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

From 0832171a2bd6389c3b39c66dc803de436df856a7 Mon Sep 17 00:00:00 2001
From: Christian Paul 
Date: Sun, 8 Apr 2018 01:38:25 -0700
Subject: [PATCH 3/8] Add German translation of the description for fastlane

---
 fastlane/metadata/android/de/full_description.txt  | 1 +
 fastlane/metadata/android/de/short_description.txt | 1 +
 2 files changed, 2 insertions(+)
 create mode 100644 fastlane/metadata/android/de/full_description.txt
 create mode 100644 fastlane/metadata/android/de/short_description.txt

diff --git a/fastlane/metadata/android/de/full_description.txt b/fastlane/metadata/android/de/full_description.txt
new file mode 100644
index 000000000..867dc62ea
--- /dev/null
+++ b/fastlane/metadata/android/de/full_description.txt
@@ -0,0 +1 @@
+NewPipe verwendet keine Bibliotheken des Google Frameworks oder der YouTube API. Es analysiert die Webseite, um die benötigten Informationen zu erlangen. Aus diesem Grund kann die App ohne die Google Services verwendet werden. Ebenso wird kein YouTube-Konto für NewPipe benötigt und es ist FLOSS.
diff --git a/fastlane/metadata/android/de/short_description.txt b/fastlane/metadata/android/de/short_description.txt
new file mode 100644
index 000000000..201fc57e9
--- /dev/null
+++ b/fastlane/metadata/android/de/short_description.txt
@@ -0,0 +1 @@
+Eine freie, leichtgewichtige YouTube App für Android

From 239e9bd3dbe15ad27a55ede45aead02c72f32157 Mon Sep 17 00:00:00 2001
From: Christian Paul 
Date: Sun, 8 Apr 2018 01:46:17 -0700
Subject: [PATCH 4/8] Rename shot_description.txt to short_description.txt

---
 .../android/en-US/{shot_description.txt => short_description.txt} | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename fastlane/metadata/android/en-US/{shot_description.txt => short_description.txt} (100%)

diff --git a/fastlane/metadata/android/en-US/shot_description.txt b/fastlane/metadata/android/en-US/short_description.txt
similarity index 100%
rename from fastlane/metadata/android/en-US/shot_description.txt
rename to fastlane/metadata/android/en-US/short_description.txt

From 35b3bf2edb62c1bafd0cbdbe36915009ccb48521 Mon Sep 17 00:00:00 2001
From: Mauricio Colli 
Date: Sun, 8 Apr 2018 08:08:19 -0300
Subject: [PATCH 5/8] 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.

From 169b6acd24d2a547fa87c087cac3808f00eb7fcc Mon Sep 17 00:00:00 2001
From: Aris Poloway 
Date: Sun, 8 Apr 2018 11:47:11 -0400
Subject: [PATCH 6/8] Clean up download dialog opening

---
 .../schabi/newpipe/fragments/detail/VideoDetailFragment.java | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

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 653ce2082..d5a830461 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
@@ -356,10 +356,9 @@ public class VideoDetailFragment
                 }
                 break;
             case R.id.detail_controls_download:
-                if (!PermissionHelper.checkStoragePermissions(activity, PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
-                    return;
+                if (PermissionHelper.checkStoragePermissions(activity, PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
+                    this.openDownloadDialog();
                 }
-                this.openDownloadDialog();
                 break;
             case R.id.detail_uploader_root_layout:
                 if (TextUtils.isEmpty(currentInfo.getUploaderUrl())) {

From 7294220727333516b4956e35f50e27e5c12fe278 Mon Sep 17 00:00:00 2001
From: Mauricio Colli 
Date: Sun, 8 Apr 2018 16:26:37 -0300
Subject: [PATCH 7/8] Revert removal of menu items

---
 .../java/org/schabi/newpipe/MainActivity.java | 10 ++++---
 app/src/main/res/menu/main_menu.xml           | 27 ++++++++++++++++---
 2 files changed, 29 insertions(+), 8 deletions(-)

diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java
index 1fdb67cf3..dd067b9b6 100644
--- a/app/src/main/java/org/schabi/newpipe/MainActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java
@@ -97,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()));
         }
@@ -336,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/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 @@
 
-
+
 
-    
+    
 
+    
+
+    
+
+    
 
\ No newline at end of file

From d494b6c934e14274524dcab44d80582fe91427ea Mon Sep 17 00:00:00 2001
From: Mauricio Colli 
Date: Sun, 8 Apr 2018 16:53:15 -0300
Subject: [PATCH 8/8] Long-click to open the downloads activity

- Closes #1263
---
 .../schabi/newpipe/fragments/detail/VideoDetailFragment.java  | 4 ++++
 1 file changed, 4 insertions(+)

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 2239cc634..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
@@ -397,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;
@@ -518,6 +521,7 @@ public class VideoDetailFragment
         detailControlsPopup.setOnClickListener(this);
         detailControlsAddToPlaylist.setOnClickListener(this);
         detailControlsDownload.setOnClickListener(this);
+        detailControlsDownload.setOnLongClickListener(this);
         relatedStreamExpandButton.setOnClickListener(this);
 
         detailControlsBackground.setLongClickable(true);