fix(android): UPnP - clean Sonos friendlyName for picker display
android / Build + lint + test (push) Successful in 4m28s

Sonos uses the friendlyName format
  'Room - Device Type - RINCON_<UDN>'

The picker was showing it verbatim, so the user saw rows like
  'Living Room - Sonos Play:1 Media Renderer - RINCON_5CAAFD79...'

Now strips on the first ' - ' for Sonos manufacturer matches, so the
chip shows just 'Living Room' / 'Kitchen' / etc. Subtitle (manufacturer
+ model) still renders below per the existing sheet design, so the
device-type info isn't lost.

Generic UPnP devices that append a '(192.168.x.x)' IP suffix get that
stripped too via an end-of-string-anchored regex. Empty / blank
friendlyName still falls back to 'Network speaker'.
This commit is contained in:
2026-06-03 14:54:39 -04:00
parent 7c11cdc4d1
commit 036da9dea8
@@ -106,7 +106,7 @@ class UpnpDiscoveryController @Inject constructor(
?.let { desc ->
UpnpRoute(
id = desc.udn,
name = desc.friendlyName.ifBlank { "Network speaker" },
name = displayName(desc.friendlyName, desc.manufacturer),
manufacturer = desc.manufacturer,
modelName = desc.modelName,
avTransportControlUrl = desc.avTransportControlUrl,
@@ -115,6 +115,30 @@ class UpnpDiscoveryController @Inject constructor(
}
}
/**
* Clean the raw UPnP friendlyName for picker display. Sonos uses
* the format `Room - Device Type - RINCON_<UDN>`; we strip
* everything after the first " - " so the chip shows just "Living
* Room" / "Kitchen" / etc. Generic UPnP devices (Yamaha, Samsung
* TV, etc.) often append a "(192.168.x.x)" IP suffix; strip that
* too. Empty / blank → "Network speaker" fallback.
*
* Device subtitle (manufacturer + model) is rendered separately
* by the picker sheet, so room-only here doesn't lose information.
*/
private fun displayName(friendlyName: String, manufacturer: String): String {
val trimmed = friendlyName.trim()
if (trimmed.isBlank()) return "Network speaker"
val byVendor = when {
manufacturer.contains("Sonos", ignoreCase = true) -> {
trimmed.substringBefore(" - ", trimmed)
}
else -> trimmed
}
val withoutIpSuffix = byVendor.replace(IP_SUFFIX_REGEX, "").trim()
return withoutIpSuffix.ifBlank { "Network speaker" }
}
private fun fetchBody(url: HttpUrl): String? {
val body = runCatching {
okHttp.newCall(Request.Builder().url(url).build()).execute().use {
@@ -123,4 +147,11 @@ class UpnpDiscoveryController @Inject constructor(
}.getOrNull().orEmpty()
return body.ifEmpty { null }
}
private companion object {
// " (192.168.0.77)" trailing host suffix some generic UPnP
// devices append. Anchored to end-of-string so it never eats
// a legitimate parenthetical inside a name.
val IP_SUFFIX_REGEX = Regex("""\s*\(\d+\.\d+\.\d+\.\d+\)$""")
}
}