diff --git a/app/src/main/java/xyz/cottongin/radio247/audio/StreamConnection.kt b/app/src/main/java/xyz/cottongin/radio247/audio/StreamConnection.kt new file mode 100644 index 0000000..7f52dce --- /dev/null +++ b/app/src/main/java/xyz/cottongin/radio247/audio/StreamConnection.kt @@ -0,0 +1,56 @@ +package xyz.cottongin.radio247.audio + +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import java.io.IOException +import java.io.InputStream +import java.time.Duration + +class ConnectionFailed(message: String, cause: Throwable? = null) : Exception(message, cause) + +class StreamConnection(private val url: String) { + private val client = OkHttpClient.Builder() + .readTimeout(Duration.ofSeconds(30)) + .build() + + var metaint: Int? = null + private set + var inputStream: InputStream? = null + private set + private var response: Response? = null + + fun open() { + val request = Request.Builder() + .url(url) + .header("Icy-MetaData", "1") + .header("User-Agent", "Radio247/1.0") + .build() + + try { + val resp = client.newCall(request).execute() + if (!resp.isSuccessful) { + resp.close() + throw ConnectionFailed("HTTP ${resp.code}") + } + response = resp + metaint = resp.header("icy-metaint")?.toIntOrNull() + inputStream = resp.body?.byteStream() + ?: throw ConnectionFailed("Empty response body") + } catch (e: IOException) { + throw ConnectionFailed("Network error", e) + } + } + + fun close() { + try { + inputStream?.close() + } catch (_: IOException) {} + try { + response?.close() + } catch (_: IOException) {} + response = null + inputStream = null + metaint = null + } +} diff --git a/app/src/test/java/xyz/cottongin/radio247/audio/StreamConnectionTest.kt b/app/src/test/java/xyz/cottongin/radio247/audio/StreamConnectionTest.kt new file mode 100644 index 0000000..06c4dde --- /dev/null +++ b/app/src/test/java/xyz/cottongin/radio247/audio/StreamConnectionTest.kt @@ -0,0 +1,123 @@ +package xyz.cottongin.radio247.audio + +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Test + +class StreamConnectionTest { + + private val server = MockWebServer() + + @After + fun tearDown() { + server.shutdown() + } + + @Test + fun sends_icy_metadata_header_and_reads_metaint_from_response() { + server.enqueue( + MockResponse() + .setHeader("icy-metaint", "16000") + .setHeader("Content-Type", "audio/mpeg") + .setBody("fake audio data") + ) + server.start() + + val conn = StreamConnection(server.url("/stream").toString()) + conn.open() + + val request = server.takeRequest() + assertEquals("1", request.getHeader("Icy-MetaData")) + assertEquals(16000, conn.metaint) + assertNotNull(conn.inputStream) + + conn.close() + } + + @Test + fun metaint_is_null_when_server_does_not_provide_it() { + server.enqueue( + MockResponse() + .setHeader("Content-Type", "audio/mpeg") + .setBody("fake audio data") + ) + server.start() + + val conn = StreamConnection(server.url("/stream").toString()) + conn.open() + + assertNull(conn.metaint) + assertNotNull(conn.inputStream) + + conn.close() + } + + @Test(expected = ConnectionFailed::class) + fun throws_ConnectionFailed_on_HTTP_error() { + server.enqueue(MockResponse().setResponseCode(404)) + server.start() + + val conn = StreamConnection(server.url("/stream").toString()) + conn.open() + } + + @Test + fun throws_ConnectionFailed_on_network_error() { + server.start() + val url = server.url("/stream").toString() + server.shutdown() + + try { + val conn = StreamConnection(url) + conn.open() + fail("Expected ConnectionFailed") + } catch (e: ConnectionFailed) { + assertTrue(e.message!!.contains("Network error")) + assertNotNull(e.cause) + } + } + + @Test + fun close_cleans_up_resources() { + server.enqueue( + MockResponse() + .setHeader("icy-metaint", "16000") + .setBody("fake audio data") + ) + server.start() + + val conn = StreamConnection(server.url("/stream").toString()) + conn.open() + assertNotNull(conn.inputStream) + assertNotNull(conn.metaint) + + conn.close() + + assertNull(conn.inputStream) + assertNull(conn.metaint) + } + + @Test + fun sends_user_agent_header() { + server.enqueue( + MockResponse() + .setHeader("icy-metaint", "16000") + .setBody("fake audio data") + ) + server.start() + + val conn = StreamConnection(server.url("/stream").toString()) + conn.open() + + val request = server.takeRequest() + assertEquals("Radio247/1.0", request.getHeader("User-Agent")) + + conn.close() + } +}