package org.schabi.newpipe.util import android.content.Context import android.view.View import android.view.View.GONE import android.view.View.INVISIBLE import android.view.View.VISIBLE import android.widget.Spinner import androidx.collection.SparseArrayCompat import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest import androidx.test.internal.runner.junit4.statement.UiThreadStatement import org.junit.Assert import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.schabi.newpipe.R import org.schabi.newpipe.extractor.MediaFormat import org.schabi.newpipe.extractor.downloader.Response import org.schabi.newpipe.extractor.stream.AudioStream import org.schabi.newpipe.extractor.stream.Stream import org.schabi.newpipe.extractor.stream.SubtitlesStream import org.schabi.newpipe.extractor.stream.VideoStream import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper @MediumTest @RunWith(AndroidJUnit4::class) class StreamItemAdapterTest { private lateinit var context: Context private lateinit var spinner: Spinner @Before fun setUp() { context = ApplicationProvider.getApplicationContext() UiThreadStatement.runOnUiThread { spinner = Spinner(context) } } @Test fun videoStreams_noSecondaryStream() { val adapter = StreamItemAdapter( getVideoStreams(true, true, true, true) ) spinner.adapter = adapter assertIconVisibility(spinner, 0, VISIBLE, VISIBLE) assertIconVisibility(spinner, 1, VISIBLE, VISIBLE) assertIconVisibility(spinner, 2, VISIBLE, VISIBLE) assertIconVisibility(spinner, 3, VISIBLE, VISIBLE) } @Test fun videoStreams_hasSecondaryStream() { val adapter = StreamItemAdapter( getVideoStreams(false, true, false, true), getAudioStreams(false, true, false, true) ) spinner.adapter = adapter assertIconVisibility(spinner, 0, GONE, GONE) assertIconVisibility(spinner, 1, GONE, GONE) assertIconVisibility(spinner, 2, GONE, GONE) assertIconVisibility(spinner, 3, GONE, GONE) } @Test fun videoStreams_Mixed() { val adapter = StreamItemAdapter( getVideoStreams(true, true, true, true, true, false, true, true), getAudioStreams(false, true, false, false, false, true, true, true) ) spinner.adapter = adapter assertIconVisibility(spinner, 0, VISIBLE, VISIBLE) assertIconVisibility(spinner, 1, GONE, INVISIBLE) assertIconVisibility(spinner, 2, VISIBLE, VISIBLE) assertIconVisibility(spinner, 3, VISIBLE, VISIBLE) assertIconVisibility(spinner, 4, VISIBLE, VISIBLE) assertIconVisibility(spinner, 5, GONE, INVISIBLE) assertIconVisibility(spinner, 6, GONE, INVISIBLE) assertIconVisibility(spinner, 7, GONE, INVISIBLE) } @Test fun subtitleStreams_noIcon() { val adapter = StreamItemAdapter( StreamItemAdapter.StreamInfoWrapper( (0 until 5).map { SubtitlesStream.Builder() .setContent("https://example.com", true) .setMediaFormat(MediaFormat.SRT) .setLanguageCode("pt-BR") .setAutoGenerated(false) .build() }, context ) ) spinner.adapter = adapter for (i in 0 until spinner.count) { assertIconVisibility(spinner, i, GONE, GONE) } } @Test fun audioStreams_noIcon() { val adapter = StreamItemAdapter( StreamItemAdapter.StreamInfoWrapper( (0 until 5).map { AudioStream.Builder() .setId(Stream.ID_UNKNOWN) .setContent("https://example.com/$it", true) .setMediaFormat(MediaFormat.OPUS) .setAverageBitrate(192) .build() }, context ) ) spinner.adapter = adapter for (i in 0 until spinner.count) { assertIconVisibility(spinner, i, GONE, GONE) } } @Test fun retrieveMediaFormatFromFileTypeHeaders() { val streams = getIncompleteAudioStreams(5) val wrapper = StreamInfoWrapper(streams, context) val retrieveMediaFormat = { stream: AudioStream, response: Response -> StreamInfoWrapper.retrieveMediaFormatFromFileTypeHeaders(stream, wrapper, response) } val helper = AssertionHelper(streams, wrapper, retrieveMediaFormat) helper.assertInvalidResponse(getResponse(mapOf(Pair("content-length", "mp3"))), 0) helper.assertInvalidResponse(getResponse(mapOf(Pair("file-type", "mp0"))), 1) helper.assertValidResponse(getResponse(mapOf(Pair("x-amz-meta-file-type", "aiff"))), 2, MediaFormat.AIFF) helper.assertValidResponse(getResponse(mapOf(Pair("file-type", "mp3"))), 3, MediaFormat.MP3) } @Test fun retrieveMediaFormatFromContentDispositionHeader() { val streams = getIncompleteAudioStreams(11) val wrapper = StreamInfoWrapper(streams, context) val retrieveMediaFormat = { stream: AudioStream, response: Response -> StreamInfoWrapper.retrieveMediaFormatFromContentDispositionHeader(stream, wrapper, response) } val helper = AssertionHelper(streams, wrapper, retrieveMediaFormat) helper.assertInvalidResponse(getResponse(mapOf(Pair("content-length", "mp3"))), 0) helper.assertInvalidResponse( getResponse(mapOf(Pair("Content-Disposition", "filename=\"train.png\""))), 1 ) helper.assertInvalidResponse( getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"data.csv\""))), 2 ) helper.assertInvalidResponse( getResponse(mapOf(Pair("Content-Disposition", "form-data; filename=\"data.csv\""))), 3 ) helper.assertInvalidResponse( getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"fieldName\"; filename*=\"filename.jpg\""))), 4 ) helper.assertValidResponse( getResponse(mapOf(Pair("Content-Disposition", "filename=\"train.ogg\""))), 5, MediaFormat.OGG ) helper.assertValidResponse( getResponse(mapOf(Pair("Content-Disposition", "some-form-data; filename=\"audio.flac\""))), 6, MediaFormat.FLAC ) helper.assertValidResponse( getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.aiff\"; filename=\"audio.aiff\""))), 7, MediaFormat.AIFF ) helper.assertValidResponse( getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"alien?\"; filename*=UTF-8''%CE%B1%CE%BB%CE%B9%CF%B5%CE%BD.m4a"))), 8, MediaFormat.M4A ) helper.assertValidResponse( getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.mp3\"; filename=\"audio.opus\"; filename*=UTF-8''alien.opus"))), 9, MediaFormat.OPUS ) helper.assertValidResponse( getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.mp3\"; filename=\"audio.opus\"; filename*=\"UTF-8''alien.opus\""))), 10, MediaFormat.OPUS ) } @Test fun retrieveMediaFormatFromContentTypeHeader() { val streams = getIncompleteAudioStreams(12) val wrapper = StreamInfoWrapper(streams, context) val retrieveMediaFormat = { stream: AudioStream, response: Response -> StreamInfoWrapper.retrieveMediaFormatFromContentTypeHeader(stream, wrapper, response) } val helper = AssertionHelper(streams, wrapper, retrieveMediaFormat) helper.assertInvalidResponse(getResponse(mapOf(Pair("content-length", "984501"))), 0) helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "audio/xyz"))), 1) helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "mp3"))), 2) helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "mp3"))), 3) helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "audio/mpeg"))), 4) helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "audio/aif"))), 5) helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "whatever"))), 6) helper.assertInvalidResponse(getResponse(mapOf()), 7) helper.assertValidResponse( getResponse(mapOf(Pair("Content-Type", "audio/flac"))), 8, MediaFormat.FLAC ) helper.assertValidResponse( getResponse(mapOf(Pair("Content-Type", "audio/wav"))), 9, MediaFormat.WAV ) helper.assertValidResponse( getResponse(mapOf(Pair("Content-Type", "audio/opus"))), 10, MediaFormat.OPUS ) helper.assertValidResponse( getResponse(mapOf(Pair("Content-Type", "audio/aiff"))), 11, MediaFormat.AIFF ) } /** * @return a list of video streams, in which their video only property mirrors the provided * [videoOnly] vararg. */ private fun getVideoStreams(vararg videoOnly: Boolean) = StreamItemAdapter.StreamInfoWrapper( videoOnly.map { VideoStream.Builder() .setId(Stream.ID_UNKNOWN) .setContent("https://example.com", true) .setMediaFormat(MediaFormat.MPEG_4) .setResolution("720p") .setIsVideoOnly(it) .build() }, context ) /** * @return a list of audio streams, containing valid and null elements mirroring the provided * [shouldBeValid] vararg. */ private fun getAudioStreams(vararg shouldBeValid: Boolean) = getSecondaryStreamsFromList( shouldBeValid.map { if (it) { AudioStream.Builder() .setId(Stream.ID_UNKNOWN) .setContent("https://example.com", true) .setMediaFormat(MediaFormat.OPUS) .setAverageBitrate(192) .build() } else { null } } ) private fun getIncompleteAudioStreams(size: Int): List { val list = ArrayList(size) for (i in 1..size) { list.add( AudioStream.Builder() .setId(Stream.ID_UNKNOWN) .setContent("https://example.com/$i", true) .build() ) } return list } /** * Checks whether the item at [position] in the [spinner] has the correct icon visibility when * it is shown in normal mode (selected) and in dropdown mode (user is choosing one of a list). */ private fun assertIconVisibility( spinner: Spinner, position: Int, normalVisibility: Int, dropDownVisibility: Int ) { spinner.setSelection(position) spinner.adapter.getView(position, null, spinner).run { Assert.assertEquals( "normal visibility (pos=[$position]) is not correct", findViewById(R.id.wo_sound_icon).visibility, normalVisibility, ) } spinner.adapter.getDropDownView(position, null, spinner).run { Assert.assertEquals( "drop down visibility (pos=[$position]) is not correct", findViewById(R.id.wo_sound_icon).visibility, dropDownVisibility ) } } /** * Helper function that builds a secondary stream list. */ private fun getSecondaryStreamsFromList(streams: List) = SparseArrayCompat?>(streams.size).apply { streams.forEachIndexed { index, stream -> val secondaryStreamHelper: SecondaryStreamHelper? = stream?.let { SecondaryStreamHelper( StreamItemAdapter.StreamInfoWrapper(streams, context), it ) } put(index, secondaryStreamHelper) } } private fun getResponse(headers: Map): Response { val listHeaders = HashMap>() headers.forEach { entry -> listHeaders[entry.key] = listOf(entry.value) } return Response(200, null, listHeaders, "", "") } /** * Helper class for assertion related to extractions of [MediaFormat]s. */ class AssertionHelper( private val streams: List, private val wrapper: StreamInfoWrapper, private val retrieveMediaFormat: (stream: T, response: Response) -> Boolean ) { /** * Assert that an invalid response does not result in wrongly extracted [MediaFormat]. */ fun assertInvalidResponse( response: Response, index: Int ) { assertFalse( "invalid header returns valid value", retrieveMediaFormat(streams[index], response) ) assertNull("Media format extracted although stated otherwise", wrapper.getFormat(index)) } /** * Assert that a valid response results in correctly extracted and handled [MediaFormat]. */ fun assertValidResponse( response: Response, index: Int, format: MediaFormat ) { assertTrue( "header was not recognized", retrieveMediaFormat(streams[index], response) ) assertEquals("Wrong media format extracted", format, wrapper.getFormat(index)) } } }