Skip to content

Commit 4146cd7

Browse files
committed
Mdns issue
1 parent 1c10863 commit 4146cd7

6 files changed

Lines changed: 48 additions & 15 deletions

File tree

app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,9 @@ android {
8181
else -> 0
8282
}
8383

84-
val vCode = 538
84+
val vCode = 541
8585
versionCode = vCode - singleAbiNum
86-
versionName = "3.0.13"
86+
versionName = "3.0.14"
8787

8888
ndk {
8989
//noinspection ChromeOsAbiSupport

app/src/main/java/com/ismartcoding/plain/mdns/MdnsHostResponder.kt

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.ismartcoding.lib.logcat.LogCat
66
import java.net.DatagramPacket
77
import java.net.DatagramSocket
88
import java.net.Inet4Address
9+
import java.net.Inet6Address
910
import java.net.InetAddress
1011
import java.net.InetSocketAddress
1112
import java.net.MulticastSocket
@@ -62,12 +63,21 @@ object MdnsHostResponder {
6263
MulticastSocket(null).apply {
6364
reuseAddress = true
6465
soTimeout = 1000
65-
bind(InetSocketAddress(MDNS_PORT))
66+
// Explicitly bind to the IPv4 wildcard.
67+
// InetSocketAddress(port) resolves to the system-preferred wildcard, which on
68+
// Samsung Android 13+ (preferIPv6Addresses=true) becomes [::]:5353 — an IPv6
69+
// socket. Joining an IPv4 multicast group on an IPv6 socket silently fails,
70+
// leaving this socket unable to receive any mDNS queries.
71+
bind(InetSocketAddress(InetAddress.getByName("0.0.0.0"), MDNS_PORT))
72+
var joinCount = 0
6673
for ((iface, ip) in candidates) {
6774
runCatching { joinGroup(groupSockAddr, iface) }
68-
.onSuccess { LogCat.d("mDNS joined ${iface.name} (${ip.hostAddress})") }
75+
.onSuccess { joinCount++; LogCat.d("mDNS joined ${iface.name} (${ip.hostAddress})") }
6976
.onFailure { LogCat.e("mDNS joinGroup ${iface.name}: ${it.message}") }
7077
}
78+
if (joinCount == 0) {
79+
LogCat.e("mDNS: no interface joined multicast group — responder will not receive queries")
80+
}
7181
}
7282
}.getOrElse {
7383
lock?.let { l -> runCatching { l.release() } }
@@ -103,7 +113,9 @@ object MdnsHostResponder {
103113
val packet = DatagramPacket(buf, buf.size)
104114
try {
105115
s.receive(packet)
106-
val senderIp = packet.address as? Inet4Address ?: continue
116+
// extractInet4Address handles both plain Inet4Address and IPv4-mapped IPv6
117+
// addresses (::ffff:x.x.x.x) that a dual-stack socket may report.
118+
val senderIp = extractInet4Address(packet.address) ?: continue
107119
val fresh = candidateInterfaces()
108120
if (fresh.isEmpty()) continue
109121
val (_, localIp) = findResponseIface(senderIp, fresh)
@@ -129,6 +141,24 @@ object MdnsHostResponder {
129141
}.onFailure { LogCat.e("mDNS send to ${dest.hostAddress}: ${it.message}") }
130142
}
131143

144+
/**
145+
* Returns the IPv4 address from [addr], unwrapping IPv4-mapped IPv6 addresses
146+
* (::ffff:x.x.x.x) that a dual-stack socket reports for IPv4 senders.
147+
*/
148+
internal fun extractInet4Address(addr: InetAddress): Inet4Address? {
149+
if (addr is Inet4Address) return addr
150+
if (addr is Inet6Address) {
151+
val b = addr.address
152+
// IPv4-mapped format: 10 zero bytes + 0xFF 0xFF + 4 IPv4 bytes
153+
if (b.size == 16 && b[0] == 0.toByte() && b[10] == 0xFF.toByte() && b[11] == 0xFF.toByte() &&
154+
b.take(10).all { it == 0.toByte() }
155+
) {
156+
return runCatching { InetAddress.getByAddress(b.copyOfRange(12, 16)) as? Inet4Address }.getOrNull()
157+
}
158+
}
159+
return null
160+
}
161+
132162
internal fun normalizeHostname(value: String): String {
133163
val trimmed = value.trim().trim('.').lowercase()
134164
if (trimmed.isEmpty()) return ""

app/src/main/java/com/ismartcoding/plain/mdns/MdnsIfaceSelector.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,17 @@ internal fun candidateInterfaces(): List<Pair<NetworkInterface, Inet4Address>> {
2020
runCatching {
2121
val ifaces = NetworkInterface.getNetworkInterfaces() ?: return result
2222
for (iface in ifaces.asSequence()) {
23-
if (!iface.isUp || iface.isLoopback || !iface.supportsMulticast()) continue
23+
if (!iface.isUp || iface.isLoopback) continue
2424
if (NetworkHelper.isVpnInterface(iface.name)) continue
2525
if (isMobileDataInterface(iface.name)) continue
26+
// Samsung's Wi-Fi driver sometimes omits IFF_MULTICAST on wlan0/ap0, causing
27+
// supportsMulticast() to return false even though the interface is perfectly
28+
// capable of multicast. Accept any interface with a Wi-Fi/Ethernet-like name
29+
// regardless of the flag, since those are always LAN interfaces.
30+
val isLanLike = iface.name.startsWith("wlan") || iface.name.startsWith("ap") ||
31+
iface.name.startsWith("eth") || iface.name.startsWith("swlan") ||
32+
iface.name.startsWith("wl") || iface.name.startsWith("p2p")
33+
if (!iface.supportsMulticast() && !isLanLike) continue
2634
val ip = iface.inetAddresses.asSequence()
2735
.filterIsInstance<Inet4Address>()
2836
.firstOrNull { !it.isLoopbackAddress } ?: continue

app/src/main/java/com/ismartcoding/plain/mdns/MdnsPacketCodec.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ internal object MdnsPacketCodec {
1818
): ByteArray? {
1919
if (query.size < 12 || ips.isEmpty()) return null
2020

21+
val flags = readU16(query, 2)
22+
// Bit 15 (QR) = 1 means this is a response, not a query. Ignore it.
23+
if (flags and 0x8000 != 0) return null
24+
2125
val qdCount = readU16(query, 4)
2226
if (qdCount <= 0) return null
2327

app/src/main/java/com/ismartcoding/plain/ui/page/audio/components/AudioActionButtons.kt

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import com.ismartcoding.plain.enums.AppFeatureType
77
import com.ismartcoding.plain.features.media.AudioMediaStoreHelper
88
import com.ismartcoding.plain.helpers.ShareHelper
99
import com.ismartcoding.plain.ui.base.ActionButtons
10-
import com.ismartcoding.plain.ui.base.IconTextCastButton
1110
import com.ismartcoding.plain.ui.base.IconTextDeleteButton
1211
import com.ismartcoding.plain.ui.base.IconTextOpenWithButton
1312
import com.ismartcoding.plain.ui.base.IconTextRenameButton
@@ -29,7 +28,6 @@ internal fun AudioActionButtons(
2928
dragSelectState: DragSelectState,
3029
context: android.content.Context,
3130
onDismiss: () -> Unit,
32-
castVM: CastViewModel? = null,
3331
) {
3432
ActionButtons {
3533
if (!audioVM.showSearchBar.value) {
@@ -43,12 +41,6 @@ internal fun AudioActionButtons(
4341
ShareHelper.shareUris(context, listOf(AudioMediaStoreHelper.getItemUri(m.id)))
4442
onDismiss()
4543
}
46-
if (castVM != null && !m.path.isUrl()) {
47-
IconTextCastButton {
48-
castVM.showCastDialog.value = true
49-
onDismiss()
50-
}
51-
}
5244
if (!m.path.isUrl()) {
5345
IconTextOpenWithButton {
5446
ShareHelper.openPathWith(context, m.path)

app/src/main/java/com/ismartcoding/plain/ui/page/audio/components/ViewAudioBottomSheet.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ fun ViewAudioBottomSheet(
7474
dragSelectState = dragSelectState,
7575
context = context,
7676
onDismiss = onDismiss,
77-
castVM = castVM,
7877
)
7978
}
8079
if (!audioVM.trash.value) {

0 commit comments

Comments
 (0)