Skip to content

Commit 629b685

Browse files
authored
Merge pull request #7516 from mauriciocolli/fix-download-dialog-selector
Fix download dialog selector layout
2 parents 860d28e + 6b1a6d2 commit 629b685

2 files changed

Lines changed: 231 additions & 7 deletions

File tree

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
package org.schabi.newpipe.util
2+
3+
import android.content.Context
4+
import android.util.SparseArray
5+
import android.view.View
6+
import android.view.View.GONE
7+
import android.view.View.INVISIBLE
8+
import android.view.View.VISIBLE
9+
import android.widget.Spinner
10+
import androidx.test.core.app.ApplicationProvider
11+
import androidx.test.ext.junit.runners.AndroidJUnit4
12+
import androidx.test.filters.MediumTest
13+
import androidx.test.internal.runner.junit4.statement.UiThreadStatement
14+
import org.junit.Assert
15+
import org.junit.Before
16+
import org.junit.Test
17+
import org.junit.runner.RunWith
18+
import org.schabi.newpipe.R
19+
import org.schabi.newpipe.extractor.MediaFormat
20+
import org.schabi.newpipe.extractor.stream.AudioStream
21+
import org.schabi.newpipe.extractor.stream.Stream
22+
import org.schabi.newpipe.extractor.stream.SubtitlesStream
23+
import org.schabi.newpipe.extractor.stream.VideoStream
24+
25+
@MediumTest
26+
@RunWith(AndroidJUnit4::class)
27+
class StreamItemAdapterTest {
28+
private lateinit var context: Context
29+
private lateinit var spinner: Spinner
30+
31+
@Before
32+
fun setUp() {
33+
context = ApplicationProvider.getApplicationContext()
34+
UiThreadStatement.runOnUiThread {
35+
spinner = Spinner(context)
36+
}
37+
}
38+
39+
@Test
40+
fun videoStreams_noSecondaryStream() {
41+
val adapter = StreamItemAdapter<VideoStream, AudioStream>(
42+
context,
43+
getVideoStreams(true, true, true, true),
44+
null
45+
)
46+
47+
spinner.adapter = adapter
48+
assertIconVisibility(spinner, 0, VISIBLE, VISIBLE)
49+
assertIconVisibility(spinner, 1, VISIBLE, VISIBLE)
50+
assertIconVisibility(spinner, 2, VISIBLE, VISIBLE)
51+
assertIconVisibility(spinner, 3, VISIBLE, VISIBLE)
52+
}
53+
54+
@Test
55+
fun videoStreams_hasSecondaryStream() {
56+
val adapter = StreamItemAdapter(
57+
context,
58+
getVideoStreams(false, true, false, true),
59+
getAudioStreams(false, true, false, true)
60+
)
61+
62+
spinner.adapter = adapter
63+
assertIconVisibility(spinner, 0, GONE, GONE)
64+
assertIconVisibility(spinner, 1, GONE, GONE)
65+
assertIconVisibility(spinner, 2, GONE, GONE)
66+
assertIconVisibility(spinner, 3, GONE, GONE)
67+
}
68+
69+
@Test
70+
fun videoStreams_Mixed() {
71+
val adapter = StreamItemAdapter(
72+
context,
73+
getVideoStreams(true, true, true, true, true, false, true, true),
74+
getAudioStreams(false, true, false, false, false, true, true, true)
75+
)
76+
77+
spinner.adapter = adapter
78+
assertIconVisibility(spinner, 0, VISIBLE, VISIBLE)
79+
assertIconVisibility(spinner, 1, GONE, INVISIBLE)
80+
assertIconVisibility(spinner, 2, VISIBLE, VISIBLE)
81+
assertIconVisibility(spinner, 3, VISIBLE, VISIBLE)
82+
assertIconVisibility(spinner, 4, VISIBLE, VISIBLE)
83+
assertIconVisibility(spinner, 5, GONE, INVISIBLE)
84+
assertIconVisibility(spinner, 6, GONE, INVISIBLE)
85+
assertIconVisibility(spinner, 7, GONE, INVISIBLE)
86+
}
87+
88+
@Test
89+
fun subtitleStreams_noIcon() {
90+
val adapter = StreamItemAdapter<SubtitlesStream, Stream>(
91+
context,
92+
StreamItemAdapter.StreamSizeWrapper(
93+
(0 until 5).map {
94+
SubtitlesStream(MediaFormat.SRT, "pt-BR", "https://example.com", false)
95+
},
96+
context
97+
),
98+
null
99+
)
100+
spinner.adapter = adapter
101+
for (i in 0 until spinner.count) {
102+
assertIconVisibility(spinner, i, GONE, GONE)
103+
}
104+
}
105+
106+
@Test
107+
fun audioStreams_noIcon() {
108+
val adapter = StreamItemAdapter<AudioStream, Stream>(
109+
context,
110+
StreamItemAdapter.StreamSizeWrapper(
111+
(0 until 5).map { AudioStream("https://example.com/$it", MediaFormat.OPUS, 192) },
112+
context
113+
),
114+
null
115+
)
116+
spinner.adapter = adapter
117+
for (i in 0 until spinner.count) {
118+
assertIconVisibility(spinner, i, GONE, GONE)
119+
}
120+
}
121+
122+
/**
123+
* @return a list of video streams, in which their video only property mirrors the provided
124+
* [videoOnly] vararg.
125+
*/
126+
private fun getVideoStreams(vararg videoOnly: Boolean) =
127+
StreamItemAdapter.StreamSizeWrapper(
128+
videoOnly.map {
129+
VideoStream("https://example.com", MediaFormat.MPEG_4, "720p", it)
130+
},
131+
context
132+
)
133+
134+
/**
135+
* @return a list of audio streams, containing valid and null elements mirroring the provided
136+
* [shouldBeValid] vararg.
137+
*/
138+
private fun getAudioStreams(vararg shouldBeValid: Boolean) =
139+
getSecondaryStreamsFromList(
140+
shouldBeValid.map {
141+
if (it) AudioStream("https://example.com", MediaFormat.OPUS, 192)
142+
else null
143+
}
144+
)
145+
146+
/**
147+
* Checks whether the item at [position] in the [spinner] has the correct icon visibility when
148+
* it is shown in normal mode (selected) and in dropdown mode (user is choosing one of a list).
149+
*/
150+
private fun assertIconVisibility(
151+
spinner: Spinner,
152+
position: Int,
153+
normalVisibility: Int,
154+
dropDownVisibility: Int
155+
) {
156+
spinner.setSelection(position)
157+
spinner.adapter.getView(position, null, spinner).run {
158+
Assert.assertEquals(
159+
"normal visibility (pos=[$position]) is not correct",
160+
findViewById<View>(R.id.wo_sound_icon).visibility,
161+
normalVisibility,
162+
)
163+
}
164+
spinner.adapter.getDropDownView(position, null, spinner).run {
165+
Assert.assertEquals(
166+
"drop down visibility (pos=[$position]) is not correct",
167+
findViewById<View>(R.id.wo_sound_icon).visibility,
168+
dropDownVisibility
169+
)
170+
}
171+
}
172+
173+
/**
174+
* Helper function that builds a secondary stream list.
175+
*/
176+
private fun <T : Stream> getSecondaryStreamsFromList(streams: List<T?>) =
177+
SparseArray<SecondaryStreamHelper<T>?>(streams.size).apply {
178+
streams.forEachIndexed { index, stream ->
179+
val secondaryStreamHelper: SecondaryStreamHelper<T>? = stream?.let {
180+
SecondaryStreamHelper(
181+
StreamItemAdapter.StreamSizeWrapper(streams, context),
182+
it
183+
)
184+
}
185+
put(index, secondaryStreamHelper)
186+
}
187+
}
188+
}

app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,21 @@ public class StreamItemAdapter<T extends Stream, U extends Stream> extends BaseA
4242
private final StreamSizeWrapper<T> streamsWrapper;
4343
private final SparseArray<SecondaryStreamHelper<U>> secondaryStreams;
4444

45+
/**
46+
* Indicates that at least one of the primary streams is an instance of {@link VideoStream},
47+
* has no audio ({@link VideoStream#isVideoOnly()} returns true) and has no secondary stream
48+
* associated with it.
49+
*/
50+
private final boolean hasAnyVideoOnlyStreamWithNoSecondaryStream;
51+
4552
public StreamItemAdapter(final Context context, final StreamSizeWrapper<T> streamsWrapper,
4653
final SparseArray<SecondaryStreamHelper<U>> secondaryStreams) {
4754
this.context = context;
4855
this.streamsWrapper = streamsWrapper;
4956
this.secondaryStreams = secondaryStreams;
50-
}
5157

52-
public StreamItemAdapter(final Context context, final StreamSizeWrapper<T> streamsWrapper,
53-
final boolean showIconNoAudio) {
54-
this(context, streamsWrapper, showIconNoAudio ? new SparseArray<>() : null);
58+
this.hasAnyVideoOnlyStreamWithNoSecondaryStream =
59+
checkHasAnyVideoOnlyStreamWithNoSecondaryStream();
5560
}
5661

5762
public StreamItemAdapter(final Context context, final StreamSizeWrapper<T> streamsWrapper) {
@@ -115,10 +120,15 @@ private View getCustomView(final int position, final View view, final ViewGroup
115120
final VideoStream videoStream = ((VideoStream) stream);
116121
qualityString = videoStream.getResolution();
117122

118-
if (secondaryStreams != null) {
123+
if (hasAnyVideoOnlyStreamWithNoSecondaryStream) {
119124
if (videoStream.isVideoOnly()) {
120-
woSoundIconVisibility = secondaryStreams.get(position) == null ? View.VISIBLE
121-
: View.INVISIBLE;
125+
woSoundIconVisibility = hasSecondaryStream(position)
126+
// It has a secondary stream associated with it, so check if it's a
127+
// dropdown view so it doesn't look out of place (missing margin)
128+
// compared to those that don't.
129+
? (isDropdownItem ? View.INVISIBLE : View.GONE)
130+
// It doesn't have a secondary stream, icon is visible no matter what.
131+
: View.VISIBLE;
122132
} else if (isDropdownItem) {
123133
woSoundIconVisibility = View.INVISIBLE;
124134
}
@@ -167,6 +177,32 @@ private View getCustomView(final int position, final View view, final ViewGroup
167177
return convertView;
168178
}
169179

180+
/**
181+
* @param position which primary stream to check.
182+
* @return whether the primary stream at position has a secondary stream associated with it.
183+
*/
184+
private boolean hasSecondaryStream(final int position) {
185+
return secondaryStreams != null && secondaryStreams.get(position) != null;
186+
}
187+
188+
/**
189+
* @return if there are any video-only streams with no secondary stream associated with them.
190+
* @see #hasAnyVideoOnlyStreamWithNoSecondaryStream
191+
*/
192+
private boolean checkHasAnyVideoOnlyStreamWithNoSecondaryStream() {
193+
for (int i = 0; i < streamsWrapper.getStreamsList().size(); i++) {
194+
final T stream = streamsWrapper.getStreamsList().get(i);
195+
if (stream instanceof VideoStream) {
196+
final boolean videoOnly = ((VideoStream) stream).isVideoOnly();
197+
if (videoOnly && !hasSecondaryStream(i)) {
198+
return true;
199+
}
200+
}
201+
}
202+
203+
return false;
204+
}
205+
170206
/**
171207
* A wrapper class that includes a way of storing the stream sizes.
172208
*

0 commit comments

Comments
 (0)