From 2fade0c1df6795cdf3fc3d5047dcc43766d67e01 Mon Sep 17 00:00:00 2001 From: mnbogner Date: Fri, 12 Jun 2026 14:11:04 -0700 Subject: [PATCH 1/9] for improved security, a swap repo config will not be saved unless it's on the same wifi network as the device --- .../fdroid/nearby/SwapWorkflowActivity.java | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java b/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java index 914893907..73a336243 100644 --- a/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java +++ b/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java @@ -463,8 +463,18 @@ public class SwapWorkflowActivity extends AppCompatActivity { Toast.makeText(this, msg, Toast.LENGTH_LONG).show(); return; } - confirmSwapConfig = new NewRepoConfig(this, intent); - checkIfNewRepoOnSameWifi(confirmSwapConfig); + + // don't save the repo config unless it's on the same network as the device + // if no config is saved, the confirmation dialog should not be displayed + NewRepoConfig repoConfig = new NewRepoConfig(this, intent); + if (!checkIfNewRepoOnSameWifi(repoConfig)) { + // show a toast here so the user is aware of the issue + String msg = getString(R.string.not_on_same_wifi, repoConfig.getSsid()); + Toast.makeText(this, msg, Toast.LENGTH_LONG).show(); + return; + } + + confirmSwapConfig = repoConfig; } private static boolean isSwapUrl(Uri uri) { @@ -789,8 +799,7 @@ public class SwapWorkflowActivity extends AppCompatActivity { if (scanResult != null) { if (scanResult.getContents() != null) { NewRepoConfig repoConfig = new NewRepoConfig(this, scanResult.getContents()); - if (repoConfig.isValidRepo()) { - checkIfNewRepoOnSameWifi(repoConfig); + if (repoConfig.isValidRepo() && checkIfNewRepoOnSameWifi(repoConfig)) { confirmSwapConfig = repoConfig; showRelevantView(); } else { @@ -824,24 +833,28 @@ public class SwapWorkflowActivity extends AppCompatActivity { } } - private void checkIfNewRepoOnSameWifi(NewRepoConfig newRepo) { + private boolean checkIfNewRepoOnSameWifi(NewRepoConfig newRepo) { // if this is a local repo, check we're on the same wifi if (!TextUtils.isEmpty(newRepo.getBssid())) { WifiManager wifiManager = ContextCompat.getSystemService(getApplicationContext(), WifiManager.class); WifiInfo wifiInfo = wifiManager.getConnectionInfo(); String bssid = wifiInfo.getBSSID(); - if (TextUtils.isEmpty(bssid)) { /* not all devices have wifi */ - return; + if (TextUtils.isEmpty(bssid)) { + // device may not have wifi + return false; } bssid = bssid.toLowerCase(Locale.ENGLISH); String newRepoBssid = Uri.decode(newRepo.getBssid()).toLowerCase(Locale.ENGLISH); if (!bssid.equals(newRepoBssid)) { - String msg = getString(R.string.not_on_same_wifi, newRepo.getSsid()); - Toast.makeText(this, msg, Toast.LENGTH_LONG).show(); + // repo config bssid doesn't match device bssid + return false; } - // TODO we should help the user to the right thing here, - // instead of just showing a message! + // repo config appears to be on same wifi as device + return true; + } else { + // repo config has empty bssid + return false; } } From 27d8b2f15925dcaec7aaa76c061f7a206a85cd8d Mon Sep 17 00:00:00 2001 From: mnbogner Date: Sat, 13 Jun 2026 14:02:18 -0700 Subject: [PATCH 2/9] added ipv6 support --- .../java/org/fdroid/fdroid/FDroidApp.java | 9 +-- .../fdroid/nearby/SwapWorkflowActivity.java | 65 ++++++++++++------- .../fdroid/nearby/WifiStateChangeService.java | 47 +++++++------- 3 files changed, 68 insertions(+), 53 deletions(-) diff --git a/app/src/full/java/org/fdroid/fdroid/FDroidApp.java b/app/src/full/java/org/fdroid/fdroid/FDroidApp.java index 70b5ce6aa..1eef90c4a 100644 --- a/app/src/full/java/org/fdroid/fdroid/FDroidApp.java +++ b/app/src/full/java/org/fdroid/fdroid/FDroidApp.java @@ -3,6 +3,7 @@ package org.fdroid.fdroid; import android.content.Context; import org.apache.commons.net.util.SubnetUtils; +import org.apache.commons.net.util.SubnetUtils6; import org.fdroid.database.Repository; import org.fdroid.index.IndexFormatVersion; @@ -23,6 +24,8 @@ public class FDroidApp { @Nullable public static volatile SubnetUtils.SubnetInfo subnetInfo; @Nullable + public static volatile SubnetUtils6.SubnetInfo subnet6Info; + @Nullable public static volatile String ssid; @Nullable public static volatile String bssid; @@ -32,14 +35,12 @@ public class FDroidApp { @SuppressWarnings("unused") public static volatile String queryString; - public static final SubnetUtils.SubnetInfo UNSET_SUBNET_INFO = - new SubnetUtils("0.0.0.0/32").getInfo(); - public static void initWifiSettings() { port = generateNewPort ? (int) (Math.random() * 10000 + 8080) : port == 0 ? 8888 : port; generateNewPort = false; ipAddressString = null; - subnetInfo = UNSET_SUBNET_INFO; + subnetInfo = null; + subnet6Info = null; ssid = null; bssid = null; } diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java b/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java index 73a336243..090a80843 100644 --- a/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java +++ b/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java @@ -71,6 +71,10 @@ import org.fdroid.fdroid.qr.CameraCharacteristicsChecker; import org.fdroid.settings.SettingsManager; import org.fdroid.ui.nearby.SwapSuccessViewModel; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.UnknownHostException; import java.util.Date; import java.util.HashMap; import java.util.Locale; @@ -478,13 +482,27 @@ public class SwapWorkflowActivity extends AppCompatActivity { } private static boolean isSwapUrl(Uri uri) { - return isSwapUrl(uri.getHost(), uri.getPort()); + return isSwapUrl(uri.getHost()); } - private static boolean isSwapUrl(String host, int port) { - return port > 1023 // only root can use <= 1023, so never a swap repo - && host.matches("[0-9.]+") // host must be an IP address - && FDroidApp.subnetInfo.isInRange(host); // on the same subnet as we are + private static boolean isSwapUrl(String host) { + try { + InetAddress hostIp = InetAddress.getByName(host); + if (hostIp instanceof Inet4Address && FDroidApp.subnetInfo != null) { + return FDroidApp.subnetInfo.isInRange(host); + } else if (hostIp instanceof Inet6Address && FDroidApp.subnet6Info != null) { + return FDroidApp.subnet6Info.isInRange(host); + } else { + // unable to verify subnet range + return false; + } + } catch (UnknownHostException e) { + // if the host can't be parsed, it isn't a valid ip address + return false; + } catch (NullPointerException e) { + // FDroidApp.subnetInfo/subnet6Info may not have been setup + return false; + } } private void promptToSelectWifiNetwork() { @@ -835,27 +853,26 @@ public class SwapWorkflowActivity extends AppCompatActivity { private boolean checkIfNewRepoOnSameWifi(NewRepoConfig newRepo) { // if this is a local repo, check we're on the same wifi - if (!TextUtils.isEmpty(newRepo.getBssid())) { - WifiManager wifiManager = ContextCompat.getSystemService(getApplicationContext(), - WifiManager.class); - WifiInfo wifiInfo = wifiManager.getConnectionInfo(); - String bssid = wifiInfo.getBSSID(); - if (TextUtils.isEmpty(bssid)) { - // device may not have wifi - return false; - } - bssid = bssid.toLowerCase(Locale.ENGLISH); - String newRepoBssid = Uri.decode(newRepo.getBssid()).toLowerCase(Locale.ENGLISH); - if (!bssid.equals(newRepoBssid)) { - // repo config bssid doesn't match device bssid - return false; - } - // repo config appears to be on same wifi as device - return true; - } else { - // repo config has empty bssid + if (TextUtils.isEmpty(newRepo.getBssid())) { + // repo may not be connected to an access point return false; } + WifiManager wifiManager = ContextCompat.getSystemService(getApplicationContext(), + WifiManager.class); + WifiInfo wifiInfo = wifiManager.getConnectionInfo(); + String bssid = wifiInfo.getBSSID(); + if (TextUtils.isEmpty(bssid)) { + // device may not be connected to an access point + return false; + } + bssid = bssid.toLowerCase(Locale.ENGLISH); + String newRepoBssid = Uri.decode(newRepo.getBssid()).toLowerCase(Locale.ENGLISH); + if (!bssid.equals(newRepoBssid)) { + // repo config bssid doesn't match device bssid, check subnet + return isSwapUrl(newRepo.getRepoUri()); + } + // repo config appears to be on same wifi as device + return true; } /** diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/WifiStateChangeService.java b/app/src/full/java/org/fdroid/fdroid/nearby/WifiStateChangeService.java index fbc169285..e565a8e5b 100644 --- a/app/src/full/java/org/fdroid/fdroid/nearby/WifiStateChangeService.java +++ b/app/src/full/java/org/fdroid/fdroid/nearby/WifiStateChangeService.java @@ -25,6 +25,7 @@ import androidx.work.Worker; import androidx.work.WorkerParameters; import org.apache.commons.net.util.SubnetUtils; +import org.apache.commons.net.util.SubnetUtils6; import org.fdroid.database.Repository; import org.fdroid.BuildConfig; import org.fdroid.fdroid.FDroidApp; @@ -33,6 +34,7 @@ import org.fdroid.fdroid.Preferences; import org.fdroid.R; import org.fdroid.fdroid.Utils; +import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.InterfaceAddress; @@ -166,24 +168,9 @@ public class WifiStateChangeService extends Worker { } if (wifiState == WifiManager.WIFI_STATE_ENABLED) { wifiInfo = wifiManager.getConnectionInfo(); - FDroidApp.ipAddressString = formatIpAddress(wifiInfo.getIpAddress()); setSsid(wifiInfo); - DhcpInfo dhcpInfo = wifiManager.getDhcpInfo(); - if (dhcpInfo != null) { - String netmask = formatIpAddress(dhcpInfo.netmask); - if (!TextUtils.isEmpty(FDroidApp.ipAddressString) && netmask != null) { - try { - FDroidApp.subnetInfo = new SubnetUtils(FDroidApp.ipAddressString, netmask).getInfo(); - } catch (IllegalArgumentException e) { - // catch mystery: "java.lang.IllegalArgumentException: Could not parse [null/24]" - e.printStackTrace(); - } - } - } - if (FDroidApp.ipAddressString == null - || FDroidApp.subnetInfo == FDroidApp.UNSET_SUBNET_INFO) { - setIpInfoFromNetworkInterface(); - } + // WifiInfo only supports ipv4 so use network interface instead + setIpInfoFromNetworkInterface(); } else if (wifiState == WifiManager.WIFI_STATE_DISABLED || wifiState == WifiManager.WIFI_STATE_DISABLING || wifiState == WifiManager.WIFI_STATE_UNKNOWN) { @@ -323,7 +310,7 @@ public class WifiStateChangeService extends Worker { for (Enumeration inetAddresses = netIf.getInetAddresses(); inetAddresses.hasMoreElements(); ) { InetAddress inetAddress = inetAddresses.nextElement(); - if (inetAddress.isLoopbackAddress() || inetAddress instanceof Inet6Address) { + if (inetAddress.isLoopbackAddress()) { continue; } if (netIf.getDisplayName().contains("wlan0") @@ -332,16 +319,26 @@ public class WifiStateChangeService extends Worker { FDroidApp.ipAddressString = inetAddress.getHostAddress(); for (InterfaceAddress address : netIf.getInterfaceAddresses()) { short networkPrefixLength = address.getNetworkPrefixLength(); - if (networkPrefixLength > 32) { - // something is giving a "/64" netmask, IPv6? - // java.lang.IllegalArgumentException: Value [64] not in range [0,32] + // assume that ipv6 prefix length > 32 + if ((inetAddress instanceof Inet4Address && networkPrefixLength > 32) + || (inetAddress instanceof Inet6Address && networkPrefixLength <= 32)) { continue; } + // include support for both ipv4 and ipv6 try { - String cidr = String.format(Locale.ENGLISH, "%s/%d", - FDroidApp.ipAddressString, networkPrefixLength); - FDroidApp.subnetInfo = new SubnetUtils(cidr).getInfo(); - break; + if (inetAddress instanceof Inet4Address) { + String cidr = String.format(Locale.ENGLISH, "%s/%d", + FDroidApp.ipAddressString, networkPrefixLength); + FDroidApp.subnetInfo = new SubnetUtils(cidr).getInfo(); + break; + } else if (inetAddress instanceof Inet6Address) { + String cidr = String.format(Locale.ENGLISH, "%s/%d", + FDroidApp.ipAddressString, networkPrefixLength); + FDroidApp.subnet6Info = new SubnetUtils6(cidr).getInfo(); + break; + } else { + Log.i(TAG, "Unexpected address type: " + inetAddress.getHostAddress()); + } } catch (IllegalArgumentException e) { if (BuildConfig.DEBUG) { e.printStackTrace(); From cb4c29783a0f31cbdd5df6d247b3e345475391a1 Mon Sep 17 00:00:00 2001 From: mnbogner Date: Sat, 13 Jun 2026 14:16:36 -0700 Subject: [PATCH 3/9] clear subnet info if wifi is disabled --- .../java/org/fdroid/fdroid/nearby/WifiStateChangeService.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/WifiStateChangeService.java b/app/src/full/java/org/fdroid/fdroid/nearby/WifiStateChangeService.java index e565a8e5b..2760a600d 100644 --- a/app/src/full/java/org/fdroid/fdroid/nearby/WifiStateChangeService.java +++ b/app/src/full/java/org/fdroid/fdroid/nearby/WifiStateChangeService.java @@ -174,6 +174,10 @@ public class WifiStateChangeService extends Worker { } else if (wifiState == WifiManager.WIFI_STATE_DISABLED || wifiState == WifiManager.WIFI_STATE_DISABLING || wifiState == WifiManager.WIFI_STATE_UNKNOWN) { + // subnet info is only used for the swap feature on wifi + // if wifi is disabled, subnet info should be cleared + FDroidApp.subnetInfo = null; + FDroidApp.subnet6Info = null; // try once to see if its a hotspot setIpInfoFromNetworkInterface(); if (FDroidApp.ipAddressString == null) { From 176a0351b1381b10de4f76147187dd6c7f1e5a69 Mon Sep 17 00:00:00 2001 From: mnbogner Date: Mon, 15 Jun 2026 15:54:02 -0700 Subject: [PATCH 4/9] restored some removed code and added some logigng based on pr feedback --- .../fdroid/nearby/SwapWorkflowActivity.java | 27 +++++++++++--- .../fdroid/nearby/WifiStateChangeService.java | 35 +++++++++++++++---- 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java b/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java index 090a80843..6f6add91a 100644 --- a/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java +++ b/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java @@ -482,25 +482,44 @@ public class SwapWorkflowActivity extends AppCompatActivity { } private static boolean isSwapUrl(Uri uri) { - return isSwapUrl(uri.getHost()); + return isSwapUrl(uri.getHost(), uri.getPort()); } - private static boolean isSwapUrl(String host) { + private static boolean isSwapUrl(String host, int port) { + Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Checking swap URL %s:%d", host, port)); + if (port < 1024) { + // only root can use < 1024, so this can't be a swap repo + Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Swap URL %s:%d is invalid because the port is out of range ( < 1024)", host, port)); + return false; + } try { InetAddress hostIp = InetAddress.getByName(host); if (hostIp instanceof Inet4Address && FDroidApp.subnetInfo != null) { - return FDroidApp.subnetInfo.isInRange(host); + if (FDroidApp.subnetInfo.isInRange(host)) { + Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Swap URL %s:%d is invalid because it is not on the subnet (ipv4)", host, port)); + return false; + } else { + return true; + } } else if (hostIp instanceof Inet6Address && FDroidApp.subnet6Info != null) { - return FDroidApp.subnet6Info.isInRange(host); + if (FDroidApp.subnet6Info.isInRange(host)) { + Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Swap URL %s:%d is invalid because it is not on the subnet (ipv6)", host, port)); + return false; + } else { + return true; + } } else { // unable to verify subnet range + Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Swap URL %s:%d is invalid because it was in an unexpected format", host, port)); return false; } } catch (UnknownHostException e) { // if the host can't be parsed, it isn't a valid ip address + Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Swap URL %s:%d is invalid because it could not be parsed", host, port)); return false; } catch (NullPointerException e) { // FDroidApp.subnetInfo/subnet6Info may not have been setup + Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Swap URL %s:%d could not be validated because the subnet info was null", host, port)); return false; } } diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/WifiStateChangeService.java b/app/src/full/java/org/fdroid/fdroid/nearby/WifiStateChangeService.java index 2760a600d..39af3b174 100644 --- a/app/src/full/java/org/fdroid/fdroid/nearby/WifiStateChangeService.java +++ b/app/src/full/java/org/fdroid/fdroid/nearby/WifiStateChangeService.java @@ -167,13 +167,31 @@ public class WifiStateChangeService extends Worker { return; } if (wifiState == WifiManager.WIFI_STATE_ENABLED) { + Utils.debugLog(TAG, "wifi enabled, get network info"); wifiInfo = wifiManager.getConnectionInfo(); + FDroidApp.ipAddressString = formatIpAddress(wifiInfo.getIpAddress()); setSsid(wifiInfo); - // WifiInfo only supports ipv4 so use network interface instead - setIpInfoFromNetworkInterface(); + DhcpInfo dhcpInfo = wifiManager.getDhcpInfo(); + if (dhcpInfo != null) { + Utils.debugLog(TAG, "get address/subnet info from dhcp"); + String netmask = formatIpAddress(dhcpInfo.netmask); + if (!TextUtils.isEmpty(FDroidApp.ipAddressString) && netmask != null) { + try { + FDroidApp.subnetInfo = new SubnetUtils(FDroidApp.ipAddressString, netmask).getInfo(); + } catch (IllegalArgumentException e) { + // catch mystery: "java.lang.IllegalArgumentException: Could not parse [null/24]" + e.printStackTrace(); + } + } + } + if (FDroidApp.ipAddressString == null || FDroidApp.subnetInfo == null) { + Utils.debugLog(TAG, "could not get address/subnet info from dhcp, try network interface"); + setIpInfoFromNetworkInterface(); + } } else if (wifiState == WifiManager.WIFI_STATE_DISABLED || wifiState == WifiManager.WIFI_STATE_DISABLING || wifiState == WifiManager.WIFI_STATE_UNKNOWN) { + Utils.debugLog(TAG, "wifi disabled/unknown, clear network info"); // subnet info is only used for the swap feature on wifi // if wifi is disabled, subnet info should be cleared FDroidApp.subnetInfo = null; @@ -304,9 +322,11 @@ public class WifiStateChangeService extends Worker { * @see netmask of WifiManager.getDhcpInfo() is always zero on Android 5.0 */ private void setIpInfoFromNetworkInterface() { + Utils.debugLog(TAG, "get address/subnet info from network interface"); try { Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); if (networkInterfaces == null) { + Utils.debugLog(TAG, "could not get address/subnet info, network interface was null"); return; } while (networkInterfaces.hasMoreElements()) { @@ -314,7 +334,9 @@ public class WifiStateChangeService extends Worker { for (Enumeration inetAddresses = netIf.getInetAddresses(); inetAddresses.hasMoreElements(); ) { InetAddress inetAddress = inetAddresses.nextElement(); + Utils.debugLog(TAG, String.format(Locale.ENGLISH, "checking address %s", inetAddress.getHostAddress())); if (inetAddress.isLoopbackAddress()) { + Utils.debugLog(TAG, "the address was the loopback address"); continue; } if (netIf.getDisplayName().contains("wlan0") @@ -326,6 +348,7 @@ public class WifiStateChangeService extends Worker { // assume that ipv6 prefix length > 32 if ((inetAddress instanceof Inet4Address && networkPrefixLength > 32) || (inetAddress instanceof Inet6Address && networkPrefixLength <= 32)) { + Utils.debugLog(TAG, String.format(Locale.ENGLISH, "invalid prefix length: %d", networkPrefixLength)); continue; } // include support for both ipv4 and ipv6 @@ -334,20 +357,20 @@ public class WifiStateChangeService extends Worker { String cidr = String.format(Locale.ENGLISH, "%s/%d", FDroidApp.ipAddressString, networkPrefixLength); FDroidApp.subnetInfo = new SubnetUtils(cidr).getInfo(); + Utils.debugLog(TAG, String.format(Locale.ENGLISH, "set ipv4 subnet info: %s", cidr)); break; } else if (inetAddress instanceof Inet6Address) { String cidr = String.format(Locale.ENGLISH, "%s/%d", FDroidApp.ipAddressString, networkPrefixLength); FDroidApp.subnet6Info = new SubnetUtils6(cidr).getInfo(); + Utils.debugLog(TAG, String.format(Locale.ENGLISH, "set ipv6 subnet info: %s", cidr)); break; - } else { - Log.i(TAG, "Unexpected address type: " + inetAddress.getHostAddress()); } } catch (IllegalArgumentException e) { if (BuildConfig.DEBUG) { e.printStackTrace(); } else { - Log.i(TAG, "Getting subnet failed: " + e.getLocalizedMessage()); + Utils.debugLog(TAG, String.format(Locale.ENGLISH, "exception thrown while getting subnet info: %s", e.getLocalizedMessage())); } } } @@ -356,7 +379,7 @@ public class WifiStateChangeService extends Worker { } } catch (NullPointerException | SocketException e) { // NetworkInterface.getNetworkInterfaces() can throw a NullPointerException internally - Log.e(TAG, "Could not get ip address", e); + Utils.debugLog(TAG, String.format(Locale.ENGLISH, "exception thrown while getting network interface: %s", e.getLocalizedMessage())); } } From 0f95122faf432e95b7849b3526d44b4242f6db46 Mon Sep 17 00:00:00 2001 From: mnbogner Date: Tue, 16 Jun 2026 17:41:51 -0700 Subject: [PATCH 5/9] fix result mismatch and observer loop --- .../fdroid/nearby/SwapWorkflowActivity.java | 47 ++++++++++++++----- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java b/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java index 6f6add91a..e89b256ae 100644 --- a/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java +++ b/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java @@ -151,6 +151,8 @@ public class SwapWorkflowActivity extends AppCompatActivity { private int currentSwapViewLayoutRes = R.layout.swap_start_swap; private final Stack backstack = new Stack<>(); + private boolean waitingForSwap = false; + private final ActivityResultLauncher requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> { if (isGranted) sendFDroidBluetooth(); @@ -496,17 +498,19 @@ public class SwapWorkflowActivity extends AppCompatActivity { InetAddress hostIp = InetAddress.getByName(host); if (hostIp instanceof Inet4Address && FDroidApp.subnetInfo != null) { if (FDroidApp.subnetInfo.isInRange(host)) { + Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Swap URL %s:%d is on the subnet (ipv4)", host, port)); + return true; + } else { Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Swap URL %s:%d is invalid because it is not on the subnet (ipv4)", host, port)); return false; - } else { - return true; } } else if (hostIp instanceof Inet6Address && FDroidApp.subnet6Info != null) { if (FDroidApp.subnet6Info.isInRange(host)) { + Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Swap URL %s:%d is on the subnet (ipv6)", host, port)); + return true; + } else { Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Swap URL %s:%d is invalid because it is not on the subnet (ipv6)", host, port)); return false; - } else { - return true; } } else { // unable to verify subnet range @@ -789,6 +793,8 @@ public class SwapWorkflowActivity extends AppCompatActivity { private void startSwappingWithPeer() { getSwapService().connectToPeer(); inflateSwapView(R.layout.swap_connecting); + // initiate swap, set flag + waitingForSwap = true; } public void swapWith(Peer peer) { @@ -871,9 +877,14 @@ public class SwapWorkflowActivity extends AppCompatActivity { } private boolean checkIfNewRepoOnSameWifi(NewRepoConfig newRepo) { - // if this is a local repo, check we're on the same wifi + // check subnet range first. sometimes returns true when bssid is empty + if (isSwapUrl(newRepo.getRepoUri())) { + return true; + } + // if this is a local repo, check whether we're on the same wifi if (TextUtils.isEmpty(newRepo.getBssid())) { // repo may not be connected to an access point + Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Swap repo may not be connected to wifi")); return false; } WifiManager wifiManager = ContextCompat.getSystemService(getApplicationContext(), @@ -882,15 +893,18 @@ public class SwapWorkflowActivity extends AppCompatActivity { String bssid = wifiInfo.getBSSID(); if (TextUtils.isEmpty(bssid)) { // device may not be connected to an access point + Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Device may not be connected to wifi")); return false; } bssid = bssid.toLowerCase(Locale.ENGLISH); String newRepoBssid = Uri.decode(newRepo.getBssid()).toLowerCase(Locale.ENGLISH); if (!bssid.equals(newRepoBssid)) { // repo config bssid doesn't match device bssid, check subnet - return isSwapUrl(newRepo.getRepoUri()); + Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Swap repo and device are not connected to the same wifi")); + return false; } // repo config appears to be on same wifi as device + Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Swap repo and device are connected to the same wifi")); return true; } @@ -1503,14 +1517,21 @@ public class SwapWorkflowActivity extends AppCompatActivity { }; private void onRepoUpdateSuccess() { - CircularProgressIndicator progressBar = container.findViewById(R.id.progress_bar); - Button tryAgainButton = container.findViewById(R.id.try_again); - if (progressBar != null && tryAgainButton != null) { - progressBar.show(); - tryAgainButton.setVisibility(View.GONE); + // there is a 5 second loop in the SwapService that updates the index, + // and a local observer watching that index that calls this method. + // if the flag is not cleared, the success screen continually pops up, + // even if the user hits the back button to exit. + if (waitingForSwap) { + waitingForSwap = false; + CircularProgressIndicator progressBar = container.findViewById(R.id.progress_bar); + Button tryAgainButton = container.findViewById(R.id.try_again); + if (progressBar != null && tryAgainButton != null) { + progressBar.show(); + tryAgainButton.setVisibility(View.GONE); + } + getSwapService().addCurrentPeerToActive(); + inflateSwapView(R.layout.swap_success); } - getSwapService().addCurrentPeerToActive(); - inflateSwapView(R.layout.swap_success); } private void onRepoUpdateError(Exception e) { From 3695c4b80c13d28856c68ea47b196e301ff74dbd Mon Sep 17 00:00:00 2001 From: mnbogner Date: Tue, 7 Jul 2026 20:16:47 -0700 Subject: [PATCH 6/9] updates based on pr suggestions to avoid unintentional dns lookups --- .../fdroid/nearby/SwapWorkflowActivity.java | 88 ++++++++++-- .../nearby/SwapWorkflowActivityTest.java | 134 ++++++++++++++++++ 2 files changed, 212 insertions(+), 10 deletions(-) create mode 100644 app/src/testFull/java/org/fdroid/fdroid/nearby/SwapWorkflowActivityTest.java diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java b/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java index e89b256ae..9d11a76a0 100644 --- a/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java +++ b/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java @@ -17,6 +17,7 @@ import android.content.ServiceConnection; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.LightingColorFilter; +import android.net.InetAddresses; import android.net.Uri; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; @@ -45,6 +46,7 @@ import androidx.annotation.LayoutRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; +import androidx.annotation.VisibleForTesting; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.SearchView; import androidx.core.content.ContextCompat; @@ -83,6 +85,7 @@ import java.util.Set; import java.util.Stack; import java.util.Timer; import java.util.TimerTask; +import java.util.regex.Pattern; import javax.inject.Inject; @@ -483,20 +486,26 @@ public class SwapWorkflowActivity extends AppCompatActivity { confirmSwapConfig = repoConfig; } - private static boolean isSwapUrl(Uri uri) { - return isSwapUrl(uri.getHost(), uri.getPort()); + static boolean isSwapUrl(Uri uri) { + // check for null argument to prevent unexpected null pointer exceptions + return uri != null && isSwapUrl(uri.getHost(), uri.getPort()); } - private static boolean isSwapUrl(String host, int port) { + @VisibleForTesting + static boolean isSwapUrl(String host, int port) { Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Checking swap URL %s:%d", host, port)); if (port < 1024) { // only root can use < 1024, so this can't be a swap repo Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Swap URL %s:%d is invalid because the port is out of range ( < 1024)", host, port)); return false; } + // verify that host is a numeric ip address to prevent a possible dns loookup + if (!isNumericAddress(host)) { + Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Swap URL %s:%d is invalid because it does not use a numeric ip address", host, port)); + return false; + } try { - InetAddress hostIp = InetAddress.getByName(host); - if (hostIp instanceof Inet4Address && FDroidApp.subnetInfo != null) { + if (isIpv4(host) && FDroidApp.subnetInfo != null) { if (FDroidApp.subnetInfo.isInRange(host)) { Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Swap URL %s:%d is on the subnet (ipv4)", host, port)); return true; @@ -504,7 +513,7 @@ public class SwapWorkflowActivity extends AppCompatActivity { Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Swap URL %s:%d is invalid because it is not on the subnet (ipv4)", host, port)); return false; } - } else if (hostIp instanceof Inet6Address && FDroidApp.subnet6Info != null) { + } else if (isIpv6(host) && FDroidApp.subnet6Info != null) { if (FDroidApp.subnet6Info.isInRange(host)) { Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Swap URL %s:%d is on the subnet (ipv6)", host, port)); return true; @@ -517,14 +526,73 @@ public class SwapWorkflowActivity extends AppCompatActivity { Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Swap URL %s:%d is invalid because it was in an unexpected format", host, port)); return false; } - } catch (UnknownHostException e) { - // if the host can't be parsed, it isn't a valid ip address - Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Swap URL %s:%d is invalid because it could not be parsed", host, port)); - return false; } catch (NullPointerException e) { // FDroidApp.subnetInfo/subnet6Info may not have been setup Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Swap URL %s:%d could not be validated because the subnet info was null", host, port)); return false; + } catch (IllegalArgumentException e) { + // in testing, isNumericAddress may return true for non-numeric adresses and + // subnetInfo/subnet6Info.isInRange fails to parse it and throws an exception + Utils.debugLog(TAG, String.format(Locale.ENGLISH, "Swap URL %s:%d could not be validated because it could not be parsed", host, port)); + return false; + } + } + + @VisibleForTesting + private static boolean isNumericAddress(String host) { + if (host == null) { + return false; + } + // use new isNumericAddress method if possible, otherwise attempt to use regex + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + return InetAddresses.isNumericAddress(host); + } else { + // regex found online, verified with unit testing + String ipv4String = "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"; + String ipv6String = "^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|" + + "^(?:[0-9a-fA-F]{1,4}:){1,7}:$|" + + "^(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}$|" + + "^(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}$|" + + "^(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}$|" + + "^(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}$|" + + "^(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}$|" + + "^[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,6}$|" + + "^:(?::[0-9a-fA-F]{1,4}){1,7}$|" + + "^::$|" + + "^(?:[0-9a-fA-F]{1,4}:){1,4}:(?:(?:25[0-5]|(?:2[0-4]|1?[0-9])?[0-9])\\.){3}(?:25[0-5]|(?:2[0-4]|1?[0-9])?[0-9])$"; + Pattern ipv4Pattern = Pattern.compile(ipv4String); + Pattern ipv6Pattern = Pattern.compile(ipv6String); + return ipv4Pattern.matcher(host).matches() || ipv6Pattern.matcher(host).matches(); + } + } + + private static boolean isIpv4(String host) { + // use new parseNumericAddress method if possible, otherwise attempt to check for format + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + try { + InetAddress i = InetAddresses.parseNumericAddress(host); + return (i instanceof Inet4Address); + } catch (IllegalArgumentException e) { + return false; + } + } else { + // no IPv4 literal should contain ":" + return !host.contains(":"); + } + } + + private static boolean isIpv6(String host) { + // use new parseNumericAddress method if possible, otherwise attempt to check for format + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + try { + InetAddress i = InetAddresses.parseNumericAddress(host); + return (i instanceof Inet6Address); + } catch (IllegalArgumentException e) { + return false; + } + } else { + // every IPv6 literal should contain ":" + return host.contains(":"); } } diff --git a/app/src/testFull/java/org/fdroid/fdroid/nearby/SwapWorkflowActivityTest.java b/app/src/testFull/java/org/fdroid/fdroid/nearby/SwapWorkflowActivityTest.java new file mode 100644 index 000000000..21a3bbfa7 --- /dev/null +++ b/app/src/testFull/java/org/fdroid/fdroid/nearby/SwapWorkflowActivityTest.java @@ -0,0 +1,134 @@ +package org.fdroid.fdroid.nearby; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.net.Uri; +import android.os.Build; + +import org.apache.commons.net.util.SubnetUtils; +import org.apache.commons.net.util.SubnetUtils6; +import org.fdroid.fdroid.FDroidApp; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Tests for {@link SwapWorkflowActivity#isSwapUrl}, which is the security gate + * that decides whether an incoming swap URL points at a repo on the local + * subnet. The host must be a numeric IP literal on our subnet; it must never + * be resolved via DNS (see merge request !1682 discussion). The two SDK + * branches (API 29+ {@code InetAddresses.isNumericAddress} vs. the legacy + * pattern fallback) are exercised separately. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Build.VERSION_CODES.P) // default: legacy pattern-match branch +public class SwapWorkflowActivityTest { + + private SubnetUtils.SubnetInfo savedSubnetInfo; + + @Before + public void setUp() { + savedSubnetInfo = FDroidApp.subnetInfo; + // pretend the device sits on 192.168.0.0/24(ipv4) 2001:db8:abcd::/64(ipv6) + FDroidApp.subnetInfo = new SubnetUtils("192.168.0.1/24").getInfo(); + FDroidApp.subnet6Info = new SubnetUtils6("2001:db8:abcd::/64").getInfo(); + } + + @After + public void tearDown() { + FDroidApp.subnetInfo = savedSubnetInfo; + } + + // ------------------------------------------------------------------ + // legacy branch (API < 29, host.matches("[0-9.]+")) + // ------------------------------------------------------------------ + + @Test + public void testValidSwapUrlLegacy() { + assertTrue(SwapWorkflowActivity.isSwapUrl("192.168.0.50", 8888)); + assertTrue(SwapWorkflowActivity.isSwapUrl("2001:db8:abcd::1234", 8888)); + } + + @Test + public void testOutOfSubnetLegacy() { + assertFalse(SwapWorkflowActivity.isSwapUrl("10.0.0.50", 8888)); + assertFalse(SwapWorkflowActivity.isSwapUrl("2010:db8:abcd::1234", 8888)); + } + + @Test + public void testHostnameIsNeverResolvedLegacy() { + // a hostname must be rejected outright, never resolved via DNS + assertFalse(SwapWorkflowActivity.isSwapUrl("example.com", 8888)); + assertFalse(SwapWorkflowActivity.isSwapUrl("localhost", 8888)); + } + + // removed testIpv6IsRejectedLegacy because ipv6 addresses are now supported + + // ------------------------------------------------------------------ + // modern branch (API 29+, android.net.InetAddresses.isNumericAddress) + // ------------------------------------------------------------------ + + @Test + @Config(sdk = Build.VERSION_CODES.R) + public void testValidSwapUrlModern() { + assertTrue(SwapWorkflowActivity.isSwapUrl("192.168.0.50", 8888)); + assertTrue(SwapWorkflowActivity.isSwapUrl("2001:db8:abcd::1234", 8888)); + } + + @Test + @Config(sdk = Build.VERSION_CODES.R) + public void testOutOfSubnetModern() { + assertFalse(SwapWorkflowActivity.isSwapUrl("10.0.0.50", 8888)); + assertFalse(SwapWorkflowActivity.isSwapUrl("2010:db8:abcd::1234", 8888)); + } + + @Test + @Config(sdk = Build.VERSION_CODES.R) + public void testHostnameIsNeverResolvedModern() { + // a hostname must be rejected outright, never resolved via DNS + // several of these are considered numeric due to a Robolectric issue, but fail + // anyway due to an IllegalArgumentException that happens further in the code + assertFalse(SwapWorkflowActivity.isSwapUrl("foo", 8888)); + assertFalse(SwapWorkflowActivity.isSwapUrl("1", 8888)); + assertFalse(SwapWorkflowActivity.isSwapUrl("example.com", 8888)); + assertFalse(SwapWorkflowActivity.isSwapUrl("localhost", 8888)); + } + + // removed testIpv6IsRejectedModern because ipv6 addresses are now supported + + // ------------------------------------------------------------------ + // port and null-safety (independent of the numeric-address branch) + // ------------------------------------------------------------------ + + @Test + public void testPrivilegedPortIsRejected() { + assertFalse(SwapWorkflowActivity.isSwapUrl("192.168.0.50", 80)); + assertFalse(SwapWorkflowActivity.isSwapUrl("192.168.0.50", 1023)); + } + + @Test + public void testNullHostIsRejected() { + assertFalse(SwapWorkflowActivity.isSwapUrl((String) null, 8888)); + } + + @Test + public void testNullUriIsRejected() { + // regression: checkIfNewRepoOnSameWifi() may pass a null repo Uri + assertFalse(SwapWorkflowActivity.isSwapUrl((Uri) null)); + } + + @Test + public void testUriWithoutHostIsRejected() { + assertFalse(SwapWorkflowActivity.isSwapUrl(Uri.parse("mailto:swap@example.com"))); + } + + @Test + public void testValidSwapUri() { + assertTrue(SwapWorkflowActivity.isSwapUrl( + Uri.parse("http://192.168.0.50:8888/fdroid/repo"))); + } +} From b707215a11e15ffb99a6f87989c04fa11cfe5da9 Mon Sep 17 00:00:00 2001 From: mnbogner Date: Sat, 13 Jun 2026 14:02:18 -0700 Subject: [PATCH 7/9] added ipv6 support --- .../java/org/fdroid/fdroid/nearby/WifiStateChangeService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/WifiStateChangeService.java b/app/src/full/java/org/fdroid/fdroid/nearby/WifiStateChangeService.java index 39af3b174..1cc7fd90a 100644 --- a/app/src/full/java/org/fdroid/fdroid/nearby/WifiStateChangeService.java +++ b/app/src/full/java/org/fdroid/fdroid/nearby/WifiStateChangeService.java @@ -169,7 +169,6 @@ public class WifiStateChangeService extends Worker { if (wifiState == WifiManager.WIFI_STATE_ENABLED) { Utils.debugLog(TAG, "wifi enabled, get network info"); wifiInfo = wifiManager.getConnectionInfo(); - FDroidApp.ipAddressString = formatIpAddress(wifiInfo.getIpAddress()); setSsid(wifiInfo); DhcpInfo dhcpInfo = wifiManager.getDhcpInfo(); if (dhcpInfo != null) { From cf996d28d26e97157e63500a15bf3d08901c513e Mon Sep 17 00:00:00 2001 From: mnbogner Date: Mon, 15 Jun 2026 15:54:02 -0700 Subject: [PATCH 8/9] restored some removed code and added some logigng based on pr feedback --- .../java/org/fdroid/fdroid/nearby/WifiStateChangeService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/WifiStateChangeService.java b/app/src/full/java/org/fdroid/fdroid/nearby/WifiStateChangeService.java index 1cc7fd90a..39af3b174 100644 --- a/app/src/full/java/org/fdroid/fdroid/nearby/WifiStateChangeService.java +++ b/app/src/full/java/org/fdroid/fdroid/nearby/WifiStateChangeService.java @@ -169,6 +169,7 @@ public class WifiStateChangeService extends Worker { if (wifiState == WifiManager.WIFI_STATE_ENABLED) { Utils.debugLog(TAG, "wifi enabled, get network info"); wifiInfo = wifiManager.getConnectionInfo(); + FDroidApp.ipAddressString = formatIpAddress(wifiInfo.getIpAddress()); setSsid(wifiInfo); DhcpInfo dhcpInfo = wifiManager.getDhcpInfo(); if (dhcpInfo != null) { From b56e58f749b94c646f1665dec503e2f6f2bc43be Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Wed, 8 Jul 2026 13:05:45 +0200 Subject: [PATCH 9/9] this code base uses integer values for SDK_INT checks --- .../java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java | 6 +++--- .../org/fdroid/fdroid/nearby/SwapWorkflowActivityTest.java | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java b/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java index 9d11a76a0..8ec776553 100644 --- a/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java +++ b/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java @@ -544,7 +544,7 @@ public class SwapWorkflowActivity extends AppCompatActivity { return false; } // use new isNumericAddress method if possible, otherwise attempt to use regex - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT >= 29) { return InetAddresses.isNumericAddress(host); } else { // regex found online, verified with unit testing @@ -568,7 +568,7 @@ public class SwapWorkflowActivity extends AppCompatActivity { private static boolean isIpv4(String host) { // use new parseNumericAddress method if possible, otherwise attempt to check for format - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT >= 29) { try { InetAddress i = InetAddresses.parseNumericAddress(host); return (i instanceof Inet4Address); @@ -583,7 +583,7 @@ public class SwapWorkflowActivity extends AppCompatActivity { private static boolean isIpv6(String host) { // use new parseNumericAddress method if possible, otherwise attempt to check for format - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT >= 29) { try { InetAddress i = InetAddresses.parseNumericAddress(host); return (i instanceof Inet6Address); diff --git a/app/src/testFull/java/org/fdroid/fdroid/nearby/SwapWorkflowActivityTest.java b/app/src/testFull/java/org/fdroid/fdroid/nearby/SwapWorkflowActivityTest.java index 21a3bbfa7..e00321637 100644 --- a/app/src/testFull/java/org/fdroid/fdroid/nearby/SwapWorkflowActivityTest.java +++ b/app/src/testFull/java/org/fdroid/fdroid/nearby/SwapWorkflowActivityTest.java @@ -73,21 +73,21 @@ public class SwapWorkflowActivityTest { // ------------------------------------------------------------------ @Test - @Config(sdk = Build.VERSION_CODES.R) + @Config(sdk = 30) public void testValidSwapUrlModern() { assertTrue(SwapWorkflowActivity.isSwapUrl("192.168.0.50", 8888)); assertTrue(SwapWorkflowActivity.isSwapUrl("2001:db8:abcd::1234", 8888)); } @Test - @Config(sdk = Build.VERSION_CODES.R) + @Config(sdk = 30) public void testOutOfSubnetModern() { assertFalse(SwapWorkflowActivity.isSwapUrl("10.0.0.50", 8888)); assertFalse(SwapWorkflowActivity.isSwapUrl("2010:db8:abcd::1234", 8888)); } @Test - @Config(sdk = Build.VERSION_CODES.R) + @Config(sdk = 30) public void testHostnameIsNeverResolvedModern() { // a hostname must be rejected outright, never resolved via DNS // several of these are considered numeric due to a Robolectric issue, but fail