diff --git a/app/src/main/java/com/amaze/filemanager/ui/notifications/FtpNotification.java b/app/src/main/java/com/amaze/filemanager/ui/notifications/FtpNotification.java index fa51a92332..105dd018a0 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/notifications/FtpNotification.java +++ b/app/src/main/java/com/amaze/filemanager/ui/notifications/FtpNotification.java @@ -103,7 +103,7 @@ public static void updateNotification(Context context, boolean noStopButton) { boolean secureConnection = sharedPreferences.getBoolean(FtpService.KEY_PREFERENCE_SECURE, FtpService.DEFAULT_SECURE); - InetAddress address = NetworkUtil.getLocalInetAddress(context); + InetAddress address = NetworkUtil.getLocalInetAddress(context, false); String address_text = "Address not found"; diff --git a/app/src/main/java/com/amaze/filemanager/utils/NetworkUtil.kt b/app/src/main/java/com/amaze/filemanager/utils/NetworkUtil.kt index c6c7bc9af7..1615226a11 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/NetworkUtil.kt +++ b/app/src/main/java/com/amaze/filemanager/utils/NetworkUtil.kt @@ -100,7 +100,10 @@ object NetworkUtil { * Caveat: doesn't handle IPv6 addresses well. Forcing return IPv4 if possible. */ @JvmStatic - fun getLocalInetAddress(context: Context): InetAddress? { + fun getLocalInetAddress( + context: Context, + requestMulticast: Boolean = false, + ): InetAddress? { if (!isConnectedToLocalNetwork(context)) { return null } @@ -112,14 +115,25 @@ object NetworkUtil { return if (ipAddress == 0) null else intToInet(ipAddress) } runCatching { - NetworkInterface.getNetworkInterfaces().iterator().forEach { netinterface -> - netinterface.inetAddresses.iterator().forEach { address -> + NetworkInterface.getNetworkInterfaces().iterator().forEach { networkInterface -> + networkInterface.inetAddresses.iterator().forEach { address -> // this is the condition that sometimes gives problems if (!address.isLoopbackAddress && !address.isLinkLocalAddress && address is Inet4Address ) { - return address + if (requestMulticast) { + if (networkInterface.supportsMulticast()) { + return address + } else { + log.warn( + "network interface {} does not support multicast", + networkInterface.displayName, + ) + } + } else { + return address + } } } } diff --git a/app/src/main/java/com/amaze/filemanager/utils/smb/SameSubnetDiscoverDeviceStrategy.kt b/app/src/main/java/com/amaze/filemanager/utils/smb/SameSubnetDiscoverDeviceStrategy.kt index 66e4739d9c..0bc1428c15 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/smb/SameSubnetDiscoverDeviceStrategy.kt +++ b/app/src/main/java/com/amaze/filemanager/utils/smb/SameSubnetDiscoverDeviceStrategy.kt @@ -95,7 +95,7 @@ class SameSubnetDiscoverDeviceStrategy : SmbDeviceScannerObservable.DiscoverDevi } private fun getNeighbourhoodHosts(): List { - val deviceAddress = NetworkUtil.getLocalInetAddress(AppConfig.getInstance()) + val deviceAddress = NetworkUtil.getLocalInetAddress(AppConfig.getInstance(), requestMulticast = true) return deviceAddress?.let { addr -> if (addr is Inet6Address) { // IPv6 neigbourhood hosts can be very big - that should use wsdd instead; hence diff --git a/app/src/main/java/com/amaze/filemanager/utils/smb/WsddDiscoverDeviceStrategy.kt b/app/src/main/java/com/amaze/filemanager/utils/smb/WsddDiscoverDeviceStrategy.kt index 3fe8e2f901..8fb92f0652 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/smb/WsddDiscoverDeviceStrategy.kt +++ b/app/src/main/java/com/amaze/filemanager/utils/smb/WsddDiscoverDeviceStrategy.kt @@ -103,7 +103,7 @@ class WsddDiscoverDeviceStrategy : SmbDeviceScannerObservable.DiscoverDeviceStra @Suppress("LabeledExpression") private fun multicastForDevice(callback: (ComputerParcelable) -> Unit) { - NetworkUtil.getLocalInetAddress(AppConfig.getInstance())?.let { addr -> + NetworkUtil.getLocalInetAddress(AppConfig.getInstance(), requestMulticast = true)?.let { addr -> val multicastAddressV4 = InetAddress.getByName(BROADCAST_IPV4) val multicastAddressV6 = InetAddress.getByName(BROADCAST_IPV6_LINK_LOCAL) diff --git a/app/src/test/java/com/amaze/filemanager/utils/NetworkUtilTest.kt b/app/src/test/java/com/amaze/filemanager/utils/NetworkUtilTest.kt new file mode 100644 index 0000000000..5f2a071a64 --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/utils/NetworkUtilTest.kt @@ -0,0 +1,267 @@ +package com.amaze.filemanager.utils + +import android.app.Service +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkInfo +import android.net.wifi.WifiInfo +import android.net.wifi.WifiManager +import android.os.Build +import android.os.Build.VERSION_CODES +import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.P +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import java.net.Inet4Address +import java.net.NetworkInterface +import java.util.Collections + +/** + * Tests for [NetworkUtil]. + */ +@RunWith(AndroidJUnit4::class) +@Config(sdk = [LOLLIPOP, P, VERSION_CODES.R]) +@Suppress("StringLiteralDuplication") +class NetworkUtilTest { + private lateinit var context: Context + private lateinit var connectivityManager: ConnectivityManager + private lateinit var wifiManager: WifiManager + private lateinit var wifiInfo: WifiInfo + + /** + * Set up the mocks. + */ + @Before + fun setUp() { + context = mockk(relaxed = true) + connectivityManager = mockk(relaxed = true) + wifiManager = mockk(relaxed = true) + wifiInfo = mockk(relaxed = true) + + every { context.applicationContext } returns context + every { + context.getSystemService(Service.CONNECTIVITY_SERVICE) + } returns connectivityManager + every { + context.getSystemService(Service.WIFI_SERVICE) + } returns wifiManager + every { wifiManager.connectionInfo } returns wifiInfo + } + + /** + * Test [NetworkUtil.isConnectedToLocalNetwork] when device is not connected. + */ + @Test + fun testGetLocalInetAddressWhenNotConnected() { + mockNetworkNotConnected() + assertNull(NetworkUtil.getLocalInetAddress(context)) + } + + /** + * Test [NetworkUtil.getLocalInetAddress] when device is connected to Wifi. + */ + @Test + fun testGetLocalInetAddressOnWifi() { + mockWifiConnected() + every { wifiInfo.ipAddress } returns 0x0F02000A // 10.0.2.15 + + val result = NetworkUtil.getLocalInetAddress(context) + assertNotNull(result) + assertEquals("10.0.2.15", result?.hostAddress) + } + + /** + * Test [NetworkUtil.getLocalInetAddress] when device is connected to Ethernet. + */ + @Test + fun testGetLocalInetAddressOnEthernet() { + mockEthernetConnected() + mockkStatic(NetworkInterface::class) + + val inetAddress = mockk() + every { inetAddress.isLoopbackAddress } returns false + every { inetAddress.isLinkLocalAddress } returns false + every { inetAddress.hostAddress } returns "192.168.1.100" + + val networkInterface = mockk() + every { networkInterface.inetAddresses } returns + Collections.enumeration(listOf(inetAddress)) + every { NetworkInterface.getNetworkInterfaces() } returns + Collections.enumeration(listOf(networkInterface)) + + val result = NetworkUtil.getLocalInetAddress(context) + assertNotNull(result) + assertEquals("192.168.1.100", result?.hostAddress) + } + + /** + * Test [NetworkUtil.getLocalInetAddress] for network interface that supports multicast. + */ + @Test + fun testGetLocalInetAddressWithMulticastSupport() { + mockEthernetConnected() + mockkStatic(NetworkInterface::class) + + val inetAddress = mockk() + every { inetAddress.isLoopbackAddress } returns false + every { inetAddress.isLinkLocalAddress } returns false + every { inetAddress.hostAddress } returns "192.168.1.1" + + val networkInterface = mockk() + every { networkInterface.supportsMulticast() } returns true + every { networkInterface.inetAddresses } returns + Collections.enumeration(listOf(inetAddress)) + every { NetworkInterface.getNetworkInterfaces() } returns + Collections.enumeration(listOf(networkInterface)) + + val result = NetworkUtil.getLocalInetAddress(context, requestMulticast = true) + assertNotNull(result) + assertEquals("192.168.1.1", result?.hostAddress) + } + + /** + * + */ + @Test + fun testGetLocalInetAddressWithoutMulticastSupport() { + mockEthernetConnected() + + val inetAddress = mockk() + every { inetAddress.isLoopbackAddress } returns false + every { inetAddress.isLinkLocalAddress } returns false + every { inetAddress.hostAddress } returns "192.168.1.100" + + val networkInterface = mockk() + every { networkInterface.supportsMulticast() } returns false + every { networkInterface.displayName } returns "eth0" + every { networkInterface.inetAddresses } returns + Collections.enumeration(listOf(inetAddress)) + + mockkStatic(NetworkInterface::class) + every { NetworkInterface.getNetworkInterfaces() } returns + Collections.enumeration(listOf(networkInterface)) + + val result = NetworkUtil.getLocalInetAddress(context, requestMulticast = true) + assertNull(result) + } + + /** + * Test [NetworkUtil.getLocalInetAddress] when there are multiple network interfaces, and we + * need to pick one that supports multicast. + */ + @Test + fun testGetLocalInetAddressWithMultipleInterfacesOneSupportsMulticast() { + mockEthernetConnected() + mockkStatic(NetworkInterface::class) + + // Create a non-multicast interface + val nonMulticastAddress = mockk() + every { nonMulticastAddress.isLoopbackAddress } returns false + every { nonMulticastAddress.isLinkLocalAddress } returns false + every { nonMulticastAddress.hostAddress } returns "192.168.1.100" + + val nonMulticastInterface = mockk() + every { nonMulticastInterface.supportsMulticast() } returns false + every { nonMulticastInterface.displayName } returns "eth0" + every { nonMulticastInterface.inetAddresses } returns + Collections.enumeration(listOf(nonMulticastAddress)) + + // Create a multicast-capable interface + val multicastAddress = mockk() + every { multicastAddress.isLoopbackAddress } returns false + every { multicastAddress.isLinkLocalAddress } returns false + every { multicastAddress.hostAddress } returns "192.168.2.100" + + val multicastInterface = mockk() + every { multicastInterface.supportsMulticast() } returns true + every { multicastInterface.displayName } returns "wlan0" + every { multicastInterface.inetAddresses } returns + Collections.enumeration(listOf(multicastAddress)) + + // Mock NetworkInterface.getNetworkInterfaces() to return both interfaces + every { NetworkInterface.getNetworkInterfaces() } returns + Collections.enumeration(listOf(nonMulticastInterface, multicastInterface)) + + val result = NetworkUtil.getLocalInetAddress(context, requestMulticast = true) + assertNotNull(result) + assertEquals("192.168.2.100", result?.hostAddress) + } + + private fun mockNetworkNotConnected() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val networkCapabilities = mockk() + every { networkCapabilities.hasTransport(any()) } returns false + every { connectivityManager.activeNetwork } returns null + every { + connectivityManager.getNetworkCapabilities(any()) + } returns networkCapabilities + } else { + every { connectivityManager.activeNetworkInfo } returns null + } + } + + private fun mockWifiConnected() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val network = mockk() + val networkCapabilities = mockk() + every { + networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) + } returns true + every { + networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) + } returns false + every { connectivityManager.activeNetwork } returns network + every { + connectivityManager.getNetworkCapabilities(network) + } returns networkCapabilities + } else { + val networkInfo = mockk() + every { networkInfo.isConnected } returns true + every { networkInfo.type } returns ConnectivityManager.TYPE_WIFI + every { connectivityManager.activeNetworkInfo } returns networkInfo + } + } + + private fun mockEthernetConnected() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val network = mockk() + val networkCapabilities = mockk() + every { + networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) + } returns false + every { + networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) + } returns true + every { connectivityManager.activeNetwork } returns network + every { + connectivityManager.getNetworkCapabilities(network) + } returns networkCapabilities + } else { + val networkInfo = mockk() + every { networkInfo.isConnected } returns true + every { networkInfo.type } returns ConnectivityManager.TYPE_ETHERNET + every { connectivityManager.activeNetworkInfo } returns networkInfo + } + } + + /** + * Clean up the mocks. + */ + @After + fun tearDown() { + unmockkAll() + } +}