feat(android): RenderingControl get/set volume
android / Build + lint + test (push) Failing after 1m33s

This commit is contained in:
2026-06-03 16:48:40 -04:00
parent 3cdb416f94
commit 8c0c4c8600
2 changed files with 100 additions and 0 deletions
@@ -0,0 +1,44 @@
package com.fabledsword.minstrel.player.output.upnp
import okhttp3.HttpUrl
/**
* RenderingControl service wrapper for hardware-volume routing while a
* UPnP route is active. GetVolume seeds an in-memory cache; SetVolume
* is invoked by NowPlayingScreen's volume-key interceptor. Clamps to
* the UPnP-standard 0..100 range.
*/
class RenderingControlClient(
private val soap: SoapClient,
private val controlUrl: HttpUrl,
) {
suspend fun getVolume(channel: String = "Master"): Int {
val args = soap.call(
controlUrl = controlUrl,
serviceType = SERVICE_TYPE,
action = "GetVolume",
args = mapOf("InstanceID" to "0", "Channel" to channel),
)
return args["CurrentVolume"]?.toIntOrNull() ?: 0
}
suspend fun setVolume(volume: Int, channel: String = "Master") {
val clamped = volume.coerceIn(VOLUME_MIN, VOLUME_MAX)
soap.call(
controlUrl = controlUrl,
serviceType = SERVICE_TYPE,
action = "SetVolume",
args = mapOf(
"InstanceID" to "0",
"Channel" to channel,
"DesiredVolume" to clamped.toString(),
),
)
}
private companion object {
const val SERVICE_TYPE = "urn:schemas-upnp-org:service:RenderingControl:1"
const val VOLUME_MIN = 0
const val VOLUME_MAX = 100
}
}
@@ -0,0 +1,56 @@
package com.fabledsword.minstrel.player.output.upnp
import kotlinx.coroutines.test.runTest
import okhttp3.OkHttpClient
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class RenderingControlClientTest {
private lateinit var server: MockWebServer
private lateinit var client: RenderingControlClient
@BeforeEach
fun setUp() {
server = MockWebServer()
server.start()
client = RenderingControlClient(SoapClient(OkHttpClient()), server.url("/RC"))
}
@AfterEach
fun tearDown() { server.shutdown() }
@Test
fun `getVolume parses CurrentVolume`() = runTest {
server.enqueue(MockResponse().setBody(
"""<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<u:GetVolumeResponse xmlns:u="urn:schemas-upnp-org:service:RenderingControl:1">
<CurrentVolume>42</CurrentVolume>
</u:GetVolumeResponse>
</s:Body>
</s:Envelope>""".trimIndent()))
assertEquals(42, client.getVolume())
}
@Test
fun `setVolume clamps and sends DesiredVolume`() = runTest {
server.enqueue(MockResponse().setBody(
"""<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<u:SetVolumeResponse
xmlns:u="urn:schemas-upnp-org:service:RenderingControl:1"/>
</s:Body>
</s:Envelope>""".trimIndent()))
client.setVolume(150)
val body = server.takeRequest().body.readUtf8()
assertTrue(body.contains("<DesiredVolume>100</DesiredVolume>")) { body }
}
}