diff --git a/README.md b/README.md index 4e00db55..8be2452d 100644 --- a/README.md +++ b/README.md @@ -261,6 +261,8 @@ that is distributed with the Firefox browser. *DuckDuckGo App Exclusions*: TrackerControl's Minimal blocking mode uses a list of excluded apps (browsers, system services, known incompatible apps) derived from DuckDuckGo's [privacy-configuration](https://github.com/duckduckgo/privacy-configuration) (Apache 2.0). +*DuckDuckGo App Tracking Protection*: TrackerControl's VPN battery optimizations (`setBlocking`, `setUnderlyingNetworks`) are informed by DuckDuckGo's [App Tracking Protection](https://github.com/duckduckgo/Android) implementation (Apache 2.0). + ## License Except where indicated otherwise, this project is licensed under [GPLv3](https://www.gnu.org/licenses/gpl-3.0.html). diff --git a/TODO.md b/TODO.md index 3163aa49..7b5caecf 100644 --- a/TODO.md +++ b/TODO.md @@ -10,13 +10,26 @@ The VPN file descriptor can be closed by `stopVPN()` while native code in `jni_r **If revisited:** The proposed fix would move the FD close into `stopNative()`, after `jni_clear()`, so the sequence becomes: `jni_stop()` -> `join thread` -> `jni_clear()` -> `close FD`. Risk is high — touches the critical VPN path, and the likely failure modes (double-close, FD leak) are harder to detect than the original race. Only worth pursuing if user reports indicate the race is actually happening. +## LAN and tethering VPN routing (resolved knowledge) + +The `VpnRoutes` exclusions for RFC1918 / CGNAT / link-local are **defense-in-depth, not load-bearing**. On modern Android, LAN access and tethering already bypass the app's VPN tun for OS-level reasons: + +- **LAN access**: When Wi-Fi is on `192.168.x.0/24`, the kernel routing table has a more-specific connected route for that subnet via `wlan0`. It outranks the VPN's `0.0.0.0/0`, so local-subnet packets never hit the tun interface regardless of what the VPN advertises. +- **Tethering (USB/hotspot)**: Tethered traffic is forwarded by `netd`/`iptables` in a separate routing context and does not traverse the owning app's `VpnService` tun at all. + +The old `lan`/`tethering`/`subnet` toggles in NetGuard were effectively no-ops on modern Android and were removed in PR #546. The new static excludes still matter for the edge case of reaching an RFC1918 destination that is *not* on the currently connected subnet (e.g. talking to `10.x` from a `192.168.x` Wi-Fi), where the kernel would otherwise prefer the VPN's default route. + +Don't re-litigate this when someone asks why LAN/tethering work without any toggle — it's intentional. + ## System apps VPN routing -Including system apps in the VPN (`include_system_vpn`) causes noticeable download speed slowdowns (e.g. Play Store). Unclear if this is inherent tun overhead or a fixable implementation issue. +Routing system apps through the VPN (`include_system_vpn`) is a noticeable battery drain. The working hypothesis is **wakeup frequency**, not tun throughput: while TC runs permanently as a VPN, any system-app background activity (Play Store updates, Play Services sync, carrier services, etc.) has to traverse the tun and wakes the packet-processing threads. Excluding system apps lets those flows bypass TC entirely so the CPU can stay idle. + +Earlier framing as a "download speed slowdown" was likely a misread of the battery symptom — raw tun throughput is probably fine. -- Investigate tun performance: profile packet processing path, test buffer size tuning +- Investigate wakeup behaviour rather than throughput: count wakeups / time-in-packet-loop with system apps included vs excluded, not MB/s. - Consider simplifying UX: current flow requires three toggles (`include_system_vpn` -> `manage_system` -> `show_system`). Could consolidate to one toggle that drives both VPN routing and UI visibility. -- Note: always routing system apps through VPN was rejected due to the performance impact, but excluding them breaks Android's "Block connections without VPN" setting. +- Note: always routing system apps through VPN was rejected due to the battery impact, but excluding them breaks Android's "Block connections without VPN" setting. ## SNI inspection for tracker detection diff --git a/app/src/main/java/eu/faircode/netguard/ActivitySettings.java b/app/src/main/java/eu/faircode/netguard/ActivitySettings.java index c91e8751..9348a1de 100644 --- a/app/src/main/java/eu/faircode/netguard/ActivitySettings.java +++ b/app/src/main/java/eu/faircode/netguard/ActivitySettings.java @@ -550,15 +550,6 @@ else if ("pause".equals(name)) getPreferenceScreen().findPreference(name) .setTitle(getString(R.string.setting_pause, prefs.getString(name, "10"))); - else if ("subnet".equals(name)) - ServiceSinkhole.reload("changed " + name, this, false); - - else if ("tethering".equals(name)) - ServiceSinkhole.reload("changed " + name, this, false); - - else if ("lan".equals(name)) - ServiceSinkhole.reload("changed " + name, this, false); - else if ("ip6".equals(name)) ServiceSinkhole.reload("changed " + name, this, false); diff --git a/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java b/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java index 8a0670ad..81a4d5c7 100644 --- a/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java +++ b/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java @@ -39,7 +39,6 @@ import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; -import android.content.res.Configuration; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -47,6 +46,7 @@ import android.graphics.Paint; import android.graphics.Path; import android.net.ConnectivityManager; +import android.net.IpPrefix; import android.net.LinkProperties; import android.net.Network; import android.net.NetworkCapabilities; @@ -101,21 +101,16 @@ import java.io.FileReader; import java.io.IOException; import java.io.InputStreamReader; -import java.math.BigInteger; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.InetSocketAddress; -import java.net.InterfaceAddress; -import java.net.NetworkInterface; import java.net.Socket; import java.net.URL; import java.net.UnknownHostException; import java.util.ArrayList; -import java.util.Collections; import java.util.Comparator; import java.util.Date; -import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -1349,42 +1344,8 @@ public static List getDns(Context context) { Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); } - // Remove local DNS servers when not routing LAN - int count = listDns.size(); - boolean lan = prefs.getBoolean("lan", false); - // boolean use_hosts = prefs.getBoolean("use_hosts", false); - if (lan - // && use_hosts - && filter) - try { - List> subnets = new ArrayList<>(); - subnets.add(new Pair<>(InetAddress.getByName("10.0.0.0"), 8)); - subnets.add(new Pair<>(InetAddress.getByName("172.16.0.0"), 12)); - subnets.add(new Pair<>(InetAddress.getByName("192.168.0.0"), 16)); - - for (Pair subnet : subnets) { - InetAddress hostAddress = subnet.first; - BigInteger host = new BigInteger(1, hostAddress.getAddress()); - - int prefix = subnet.second; - BigInteger mask = BigInteger.valueOf(-1).shiftLeft(hostAddress.getAddress().length * 8 - prefix); - - for (InetAddress dns : new ArrayList<>(listDns)) - if (hostAddress.getAddress().length == dns.getAddress().length) { - BigInteger ip = new BigInteger(1, dns.getAddress()); - - if (host.and(mask).equals(ip.and(mask))) { - Log.i(TAG, "Local DNS server host=" + hostAddress + "/" + prefix + " dns=" + dns); - listDns.remove(dns); - } - } - } - } catch (Throwable ex) { - Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); - } - - // Fallback to default DNS servers if none remain - if (listDns.size() == 0) + // Fallback DNS servers if none found + if (listDns.isEmpty()) try { listDns.add(InetAddress.getByName(net.kollnig.missioncontrol.BuildConfig.DEFAULT_DNS_IPV4)); listDns.add(InetAddress.getByName(net.kollnig.missioncontrol.BuildConfig.DEFAULT_DNS_IPV4_2)); @@ -1406,13 +1367,15 @@ private ParcelFileDescriptor startVPN(Builder builder) throws SecurityException try { ParcelFileDescriptor pfd = builder.establish(); - // Set underlying network - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && false) { + // Set underlying network so Android can correctly assess connectivity, + // metering, and network scoring for the VPN. + // Mirrors DuckDuckGo ATP approach (Apache 2.0). + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { ConnectivityManager cm = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE); Network active = (cm == null ? null : cm.getActiveNetwork()); if (active != null) { - Log.i(TAG, "Setting underlying network=" + active + " " + cm.getNetworkInfo(active)); - setUnderlyingNetworks(new Network[] { active }); + Log.i(TAG, "Setting underlying network=" + active); + setUnderlyingNetworks(new Network[]{active}); } } @@ -1427,9 +1390,6 @@ private ParcelFileDescriptor startVPN(Builder builder) throws SecurityException private Builder getBuilder(List listAllowed, List listRule) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); - boolean subnet = prefs.getBoolean("subnet", false); - boolean tethering = prefs.getBoolean("tethering", false); - boolean lan = prefs.getBoolean("lan", false); boolean ip6 = prefs.getBoolean("ip6", true); boolean filter = prefs.getBoolean("filter", true); boolean includeSystem = prefs.getBoolean("include_system_vpn", false); @@ -1438,9 +1398,15 @@ private Builder getBuilder(List listAllowed, List listRule) { Builder builder = new Builder(); builder.setSession(getString(R.string.app_name)); + // Match the physical network's metered status so apps behave the + // same as without the VPN. (Android defaults VPNs to metered.) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) builder.setMetered(Util.isMeteredNetwork(this)); + // Use blocking I/O on the TUN file descriptor for CPU efficiency + // (avoids polling when there is no traffic) + builder.setBlocking(true); + // VPN address String vpn4 = prefs.getString("vpn4", "10.1.10.1"); Log.i(TAG, "Using VPN4=" + vpn4); @@ -1474,154 +1440,65 @@ private Builder getBuilder(List listAllowed, List listRule) { Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); } - // Subnet routing - if (subnet) { - // Exclude IP ranges - List listExclude = new ArrayList<>(); - listExclude.add(new IPUtil.CIDR("127.0.0.0", 8)); // localhost - - if (tethering && !lan) { - // USB tethering 192.168.42.x - // Wi-Fi tethering 192.168.43.x - listExclude.add(new IPUtil.CIDR("192.168.42.0", 23)); - // Bluetooth tethering 192.168.44.x - listExclude.add(new IPUtil.CIDR("192.168.44.0", 24)); - // Wi-Fi direct 192.168.49.x - listExclude.add(new IPUtil.CIDR("192.168.49.0", 24)); - - try { - Enumeration nis = NetworkInterface.getNetworkInterfaces(); - if (nis != null) - while (nis.hasMoreElements()) { - NetworkInterface ni = nis.nextElement(); - if (ni != null && !ni.isLoopback() && ni.isUp() && - ni.getName() != null && ni.getName().startsWith("ap_br_wlan")) { - List ias = ni.getInterfaceAddresses(); - if (ias != null) - for (InterfaceAddress ia : ias) - if (ia.getAddress() instanceof Inet4Address) - listExclude.add(new IPUtil.CIDR(ia.getAddress().getHostAddress(), 24)); - } - } - } catch (Throwable ex) { - Log.e(TAG, ex.toString()); - } - } - - if (lan) { - // https://tools.ietf.org/html/rfc1918 - listExclude.add(new IPUtil.CIDR("10.0.0.0", 8)); - listExclude.add(new IPUtil.CIDR("172.16.0.0", 12)); - listExclude.add(new IPUtil.CIDR("192.168.0.0", 16)); + // Static routes covering all public IPv4 space, excluding private, + // reserved, and carrier Wi-Fi calling ranges. + // Mirrors DuckDuckGo ATP approach (Apache 2.0). + for (IPUtil.CIDR route : VpnRoutes.getRoutes()) + try { + builder.addRoute(route.address, route.prefix); + } catch (Throwable ex) { + Log.e(TAG, "addRoute " + route + ": " + ex); } - if (!filter) { - for (InetAddress dns : getDns(ServiceSinkhole.this)) - if (dns instanceof Inet4Address) - listExclude.add(new IPUtil.CIDR(dns.getHostAddress(), 32)); - - String dns_specifier = Util.getPrivateDnsSpecifier(ServiceSinkhole.this); - if (!TextUtils.isEmpty(dns_specifier)) + // Add /32 host routes for local DNS servers so their traffic enters the + // tunnel (where TC can filter it) even though LAN is otherwise excluded. + // This preserves compatibility with local DNS setups like Pi-hole. + if (filter) + for (InetAddress dns : getDns(ServiceSinkhole.this)) + if (dns instanceof Inet4Address && dns.isSiteLocalAddress()) try { - Log.i(TAG, "Resolving private dns=" + dns_specifier); - for (InetAddress pdns : InetAddress.getAllByName(dns_specifier)) - if (pdns instanceof Inet4Address) - listExclude.add(new IPUtil.CIDR(pdns.getHostAddress(), 32)); + Log.i(TAG, "Adding host route for local DNS=" + dns.getHostAddress()); + builder.addRoute(dns, 32); } catch (Throwable ex) { - Log.e(TAG, ex.toString()); + Log.e(TAG, "addRoute DNS " + dns + ": " + ex); } - } - - // https://en.wikipedia.org/wiki/Mobile_country_code - Configuration config = getResources().getConfiguration(); - - // T-Mobile Wi-Fi calling - if (config.mcc == 310 && (config.mnc == 160 || - config.mnc == 200 || - config.mnc == 210 || - config.mnc == 220 || - config.mnc == 230 || - config.mnc == 240 || - config.mnc == 250 || - config.mnc == 260 || - config.mnc == 270 || - config.mnc == 310 || - config.mnc == 490 || - config.mnc == 660 || - config.mnc == 800)) { - listExclude.add(new IPUtil.CIDR("66.94.2.0", 24)); - listExclude.add(new IPUtil.CIDR("66.94.6.0", 23)); - listExclude.add(new IPUtil.CIDR("66.94.8.0", 22)); - listExclude.add(new IPUtil.CIDR("208.54.0.0", 16)); - } - - // Verizon wireless calling - if ((config.mcc == 310 && - (config.mnc == 4 || - config.mnc == 5 || - config.mnc == 6 || - config.mnc == 10 || - config.mnc == 12 || - config.mnc == 13 || - config.mnc == 350 || - config.mnc == 590 || - config.mnc == 820 || - config.mnc == 890 || - config.mnc == 910)) - || - (config.mcc == 311 && (config.mnc == 12 || - config.mnc == 110 || - (config.mnc >= 270 && config.mnc <= 289) || - config.mnc == 390 || - (config.mnc >= 480 && config.mnc <= 489) || - config.mnc == 590)) - || - (config.mcc == 312 && (config.mnc == 770))) { - listExclude.add(new IPUtil.CIDR("66.174.0.0", 16)); // 66.174.0.0 - 66.174.255.255 - listExclude.add(new IPUtil.CIDR("66.82.0.0", 15)); // 69.82.0.0 - 69.83.255.255 - listExclude.add(new IPUtil.CIDR("69.96.0.0", 13)); // 69.96.0.0 - 69.103.255.255 - listExclude.add(new IPUtil.CIDR("70.192.0.0", 11)); // 70.192.0.0 - 70.223.255.255 - listExclude.add(new IPUtil.CIDR("97.128.0.0", 9)); // 97.128.0.0 - 97.255.255.255 - listExclude.add(new IPUtil.CIDR("174.192.0.0", 9)); // 174.192.0.0 - 174.255.255.255 - listExclude.add(new IPUtil.CIDR("72.96.0.0", 9)); // 72.96.0.0 - 72.127.255.255 - listExclude.add(new IPUtil.CIDR("75.192.0.0", 9)); // 75.192.0.0 - 75.255.255.255 - listExclude.add(new IPUtil.CIDR("97.0.0.0", 10)); // 97.0.0.0 - 97.63.255.255 - } - - // SFR MMS - if (config.mnc == 10 && config.mcc == 208) - listExclude.add(new IPUtil.CIDR("10.151.0.0", 24)); - - // Broadcast - listExclude.add(new IPUtil.CIDR("224.0.0.0", 3)); - - Collections.sort(listExclude); + // Dynamically exclude carrier ePDG IPs so Wi-Fi calling works globally. + // ePDG domains follow 3GPP standard: epdg.epc.mnc{MNC}.mcc{MCC}.pub.3gppnetwork.org + // TC excludes itself from the VPN (addDisallowedApplication), so this DNS resolution + // goes through the physical network — using the carrier's own DNS on cellular, + // which avoids geo-fencing issues. Re-resolved on each VPN rebuild (network switch). + // Uses a short timeout to avoid blocking VPN startup if resolution is slow. + // Requires Android 13+ (API 33) for excludeRoute(). + if (Build.VERSION.SDK_INT >= 33) try { - InetAddress start = InetAddress.getByName("0.0.0.0"); - for (IPUtil.CIDR exclude : listExclude) { - Log.i(TAG, "Exclude " + exclude.getStart().getHostAddress() + "..." - + exclude.getEnd().getHostAddress()); - for (IPUtil.CIDR include : IPUtil.toCIDR(start, IPUtil.minus1(exclude.getStart()))) - try { - builder.addRoute(include.address, include.prefix); - } catch (Throwable ex) { - Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); - } - start = IPUtil.plus1(exclude.getEnd()); - } - String end = (lan ? "255.255.255.254" : "255.255.255.255"); - for (IPUtil.CIDR include : IPUtil.toCIDR("224.0.0.0", end)) - try { - builder.addRoute(include.address, include.prefix); - } catch (Throwable ex) { - Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + TelephonyManager tm = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); + String simOperator = (tm == null ? null : tm.getSimOperator()); + if (simOperator != null && simOperator.length() >= 5) { + String mcc = simOperator.substring(0, 3); + String mnc = simOperator.substring(3); + if (mnc.length() == 2) + mnc = "0" + mnc; + String epdgDomain = "epdg.epc.mnc" + mnc + ".mcc" + mcc + ".pub.3gppnetwork.org"; + Log.i(TAG, "Resolving ePDG domain=" + epdgDomain); + + // Resolve with 1.5s timeout to avoid blocking VPN startup + final String domain = epdgDomain; + java.util.concurrent.Future future = + java.util.concurrent.Executors.newSingleThreadExecutor() + .submit(() -> InetAddress.getAllByName(domain)); + InetAddress[] addrs = future.get(1500, java.util.concurrent.TimeUnit.MILLISECONDS); + for (InetAddress addr : addrs) { + Log.i(TAG, "Excluding ePDG address=" + addr.getHostAddress()); + builder.excludeRoute(new IpPrefix(addr, addr instanceof Inet4Address ? 32 : 128)); } - } catch (UnknownHostException ex) { - Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + } catch (java.util.concurrent.TimeoutException ex) { + Log.i(TAG, "ePDG resolution timed out, skipping"); + } catch (Throwable ex) { + // Resolution may fail (no SIM, airplane mode, non-standard carrier) — not fatal + Log.i(TAG, "ePDG resolution skipped: " + ex.getMessage()); } - } else - builder.addRoute("0.0.0.0", 0); Log.i(TAG, "IPv6=" + ip6); if (ip6) diff --git a/app/src/main/java/eu/faircode/netguard/VpnRoutes.java b/app/src/main/java/eu/faircode/netguard/VpnRoutes.java new file mode 100644 index 00000000..29430d62 --- /dev/null +++ b/app/src/main/java/eu/faircode/netguard/VpnRoutes.java @@ -0,0 +1,146 @@ +/* + * This file is part of TrackerControl. + * + * TrackerControl is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * TrackerControl is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with TrackerControl. If not, see . + * + * VPN routing approach informed by DuckDuckGo App Tracking Protection (Apache 2.0). + * See: https://github.com/duckduckgo/Android + */ + +package eu.faircode.netguard; + +import android.util.Log; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Static pre-computed VPN routes covering all public IPv4 address space, + * excluding private, reserved, and carrier Wi-Fi calling ranges. + * + * Routes are computed once on first access from a fixed exclusion list, + * avoiding per-rebuild dynamic computation overhead. + */ +public class VpnRoutes { + private static final String TAG = "TrackerControl.VpnRoutes"; + private static volatile List cachedRoutes; + + // Ranges excluded from VPN routing (must not overlap) + private static final String[][] EXCLUDED_RANGES = { + // Reserved and private ranges + {"0.0.0.0", "8"}, // Current network (RFC 1122) + {"10.0.0.0", "8"}, // Private (RFC 1918) + {"100.64.0.0", "10"}, // CGNAT (RFC 6598) + {"127.0.0.0", "8"}, // Loopback (RFC 1122) + {"169.254.0.0", "16"}, // Link-local (RFC 3927) + {"172.16.0.0", "12"}, // Private (RFC 1918) + {"192.168.0.0", "16"}, // Private (RFC 1918) + {"224.0.0.0", "3"}, // Multicast (224/4) + reserved (240/4) + + // T-Mobile Wi-Fi calling + {"66.94.2.0", "24"}, + {"66.94.6.0", "23"}, + {"66.94.8.0", "22"}, + {"208.54.0.0", "16"}, + + // Verizon Wi-Fi calling + {"66.82.0.0", "15"}, + {"66.174.0.0", "16"}, + {"69.96.0.0", "13"}, + {"70.192.0.0", "11"}, + {"72.96.0.0", "9"}, + {"75.192.0.0", "9"}, + {"97.0.0.0", "10"}, + {"97.128.0.0", "9"}, + {"174.192.0.0", "9"}, + }; + + /** + * Returns the list of CIDR routes to add to the VPN builder. + * Computed once and cached for all subsequent calls. + */ + public static List getRoutes() { + if (cachedRoutes == null) { + synchronized (VpnRoutes.class) { + if (cachedRoutes == null) { + cachedRoutes = computeRoutes(); + } + } + } + return cachedRoutes; + } + + private static List computeRoutes() { + // Build sorted exclusion list + List excludes = new ArrayList<>(); + for (String[] range : EXCLUDED_RANGES) { + excludes.add(new IPUtil.CIDR(range[0], Integer.parseInt(range[1]))); + } + Collections.sort(excludes); + + // Compute complement: all IP ranges NOT in the exclusion list + List routes = new ArrayList<>(); + try { + long startLong = 0; // 0.0.0.0 + for (IPUtil.CIDR exclude : excludes) { + long excludeStartLong = inetToLong(exclude.getStart()); + if (excludeStartLong > startLong) { + InetAddress gapStart = longToInet(startLong); + InetAddress gapEnd = IPUtil.minus1(exclude.getStart()); + routes.addAll(IPUtil.toCIDR(gapStart, gapEnd)); + } + long excludeEndLong = inetToLong(exclude.getEnd()); + if (excludeEndLong < 0xFFFFFFFFL) { + startLong = excludeEndLong + 1; + } else { + startLong = 0xFFFFFFFFL + 1; // Past end of IPv4 space + break; + } + } + // Any remaining space after last exclusion (none if 224.0.0.0/3 is last) + if (startLong <= 0xFFFFFFFFL) { + routes.addAll(IPUtil.toCIDR(longToInet(startLong), longToInet(0xFFFFFFFFL))); + } + } catch (UnknownHostException ex) { + Log.e(TAG, "Failed to compute routes: " + ex); + } + + Log.i(TAG, "Computed " + routes.size() + " VPN routes from " + + EXCLUDED_RANGES.length + " exclusions"); + return Collections.unmodifiableList(routes); + } + + private static long inetToLong(InetAddress addr) { + long result = 0; + for (byte b : addr.getAddress()) + result = result << 8 | (b & 0xFF); + return result; + } + + private static InetAddress longToInet(long addr) { + try { + byte[] b = new byte[4]; + for (int i = 3; i >= 0; i--) { + b[i] = (byte) (addr & 0xFF); + addr >>= 8; + } + return InetAddress.getByAddress(b); + } catch (UnknownHostException ignore) { + return null; + } + } +} diff --git a/app/src/main/java/net/kollnig/missioncontrol/InsightsActivity.kt b/app/src/main/java/net/kollnig/missioncontrol/InsightsActivity.kt index f4eed554..493add5e 100644 --- a/app/src/main/java/net/kollnig/missioncontrol/InsightsActivity.kt +++ b/app/src/main/java/net/kollnig/missioncontrol/InsightsActivity.kt @@ -35,11 +35,15 @@ import android.view.View import android.view.animation.DecelerateInterpolator import android.widget.LinearLayout import android.widget.ProgressBar +import android.widget.ScrollView import android.widget.TextView import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.core.content.FileProvider +import androidx.core.graphics.Insets +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat import androidx.lifecycle.lifecycleScope import com.google.android.material.floatingactionbutton.FloatingActionButton import eu.faircode.netguard.Util @@ -111,6 +115,16 @@ class InsightsActivity : AppCompatActivity() { shareInsights() } + // Pad the scroll content so the last card isn't hidden behind the navigation bar. + val scroll = findViewById(R.id.insightsScroll) + scroll.clipToPadding = false + val scrollInitialBottom = scroll.paddingBottom + ViewCompat.setOnApplyWindowInsetsListener(scroll) { v, insets -> + val sysBars: Insets = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(v.paddingLeft, v.paddingTop, v.paddingRight, scrollInitialBottom + sysBars.bottom) + insets + } + dataProvider = InsightsDataProvider(this) loadData() diff --git a/app/src/main/res/layout/activity_insights.xml b/app/src/main/res/layout/activity_insights.xml index 618d9292..c51613d6 100644 --- a/app/src/main/res/layout/activity_insights.xml +++ b/app/src/main/res/layout/activity_insights.xml @@ -4,10 +4,10 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - android:fitsSystemWindows="true" android:background="?android:attr/colorBackground"> diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index b5d1dfb6..01a7b0ca 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -51,23 +51,6 @@ android:key="update_check" android:summary="@string/summary_update" android:title="@string/setting_update" /> - - -