From c5f1e3c975d457041fefe88797002698487cb2b5 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Mon, 22 Jun 2026 17:46:34 +0000 Subject: [PATCH 1/4] =?UTF-8?q?feat(network):=20Setup=20DNS=20logic=20?= =?UTF-8?q?=E2=80=94=20probe,=20configure=20use=20case,=20ViewModel=20(PR?= =?UTF-8?q?=20B,=20part=202/2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Presentation + connectivity layer for the Usage 'Setup DNS' panel (UI wiring in a follow-up commit on this branch): - domain: DnsConnectivityProbe port + ConfigureDnsUseCase (validate -> netplan-try probe -> save custom, or revert to defaults). Unit-tested (ConfigureDnsUseCaseTest: applied / invalid / unreachable-reverts / secondary-fallback). - data: UdpDnsConnectivityProbe (tiny UDP DNS A-query to the server:53, IPv4/IPv6, never throws — timeout/unreachable = not reachable). - presentation: DnsSettingsUiState (IDLE/TESTING/APPLIED/INVALID/UNREACHABLE), DnsSettingsViewModel (off-main-thread orchestration; uncheck reverts + clears to defaults), DnsSettingsViewModelFactory (manual DI, mirrors RootfsViewModelFactory). Builds on PR A (part1). UI (UsageFragment + fragment_usage.xml) is the next commit. --- .../network/data/UdpDnsConnectivityProbe.java | 88 ++++++++++++++++++ .../network/domain/ConfigureDnsUseCase.java | 66 ++++++++++++++ .../network/domain/DnsConnectivityProbe.java | 24 +++++ .../presentation/DnsSettingsUiState.java | 55 ++++++++++++ .../presentation/DnsSettingsViewModel.java | 89 +++++++++++++++++++ .../DnsSettingsViewModelFactory.java | 47 ++++++++++ .../domain/ConfigureDnsUseCaseTest.java | 65 ++++++++++++++ 7 files changed, 434 insertions(+) create mode 100644 controller/app/src/main/java/org/iiab/controller/network/data/UdpDnsConnectivityProbe.java create mode 100644 controller/app/src/main/java/org/iiab/controller/network/domain/ConfigureDnsUseCase.java create mode 100644 controller/app/src/main/java/org/iiab/controller/network/domain/DnsConnectivityProbe.java create mode 100644 controller/app/src/main/java/org/iiab/controller/network/presentation/DnsSettingsUiState.java create mode 100644 controller/app/src/main/java/org/iiab/controller/network/presentation/DnsSettingsViewModel.java create mode 100644 controller/app/src/main/java/org/iiab/controller/network/presentation/DnsSettingsViewModelFactory.java create mode 100644 controller/app/src/test/java/org/iiab/controller/network/domain/ConfigureDnsUseCaseTest.java diff --git a/controller/app/src/main/java/org/iiab/controller/network/data/UdpDnsConnectivityProbe.java b/controller/app/src/main/java/org/iiab/controller/network/data/UdpDnsConnectivityProbe.java new file mode 100644 index 0000000..3549a7c --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/network/data/UdpDnsConnectivityProbe.java @@ -0,0 +1,88 @@ +/* + * ============================================================================ + * Name : UdpDnsConnectivityProbe.java + * Author : AppDevForAll + * Copyright : Copyright (c) 2026 AppDevForAll + * Description : UDP DNS-query probe: sends a query to the server and waits for a reply. + * ============================================================================ + */ +package org.iiab.controller.network.data; + +import android.util.Log; + +import org.iiab.controller.network.domain.DnsConnectivityProbe; + +import java.io.ByteArrayOutputStream; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.nio.charset.StandardCharsets; + +/** + * {@link DnsConnectivityProbe} that sends a tiny DNS A-query (for a fixed, stable + * name) over UDP to the given server on port 53 and waits for a valid reply. + * Pure {@code java.net}; works for IPv4 and IPv6 servers. Never throws — any + * error (timeout, unreachable, malformed) means "not reachable". + */ +public final class UdpDnsConnectivityProbe implements DnsConnectivityProbe { + + private static final String TAG = "IIAB-DNS"; + private static final String PROBE_NAME = "example.com"; + private static final int DNS_PORT = 53; + + @Override + public boolean isReachable(String dnsServer, long timeoutMs) { + if (dnsServer == null || dnsServer.trim().isEmpty()) return false; + DatagramSocket socket = null; + try { + byte[] query = buildQuery(PROBE_NAME); + InetAddress addr = InetAddress.getByName(dnsServer.trim()); + socket = new DatagramSocket(); + socket.setSoTimeout((int) Math.max(1, Math.min(timeoutMs, Integer.MAX_VALUE))); + socket.send(new DatagramPacket(query, query.length, addr, DNS_PORT)); + + byte[] buf = new byte[512]; + DatagramPacket response = new DatagramPacket(buf, buf.length); + socket.receive(response); + + if (response.getLength() < 12) return false; + boolean idMatches = buf[0] == query[0] && buf[1] == query[1]; + boolean isResponse = (buf[2] & 0x80) != 0; + int rcode = buf[3] & 0x0F; + return idMatches && isResponse && rcode == 0; + } catch (Exception e) { + Log.i(TAG, "DNS probe to " + dnsServer + " failed: " + e.getMessage()); + return false; + } finally { + if (socket != null) socket.close(); + } + } + + private static byte[] buildQuery(String name) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + int id = (int) (System.nanoTime() & 0xFFFF); + out.write((id >> 8) & 0xFF); + out.write(id & 0xFF); + out.write(0x01); + out.write(0x00); + out.write(0x00); + out.write(0x01); + out.write(0x00); + out.write(0x00); + out.write(0x00); + out.write(0x00); + out.write(0x00); + out.write(0x00); + for (String label : name.split("\\.")) { + byte[] b = label.getBytes(StandardCharsets.US_ASCII); + out.write(b.length); + out.write(b, 0, b.length); + } + out.write(0x00); + out.write(0x00); + out.write(0x01); + out.write(0x00); + out.write(0x01); + return out.toByteArray(); + } +} diff --git a/controller/app/src/main/java/org/iiab/controller/network/domain/ConfigureDnsUseCase.java b/controller/app/src/main/java/org/iiab/controller/network/domain/ConfigureDnsUseCase.java new file mode 100644 index 0000000..6af70d2 --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/network/domain/ConfigureDnsUseCase.java @@ -0,0 +1,66 @@ +/* + * ============================================================================ + * Name : ConfigureDnsUseCase.java + * Author : AppDevForAll + * Copyright : Copyright (c) 2026 AppDevForAll + * Description : Validate, netplan-try probe, then save (or revert to defaults). + * ============================================================================ + */ +package org.iiab.controller.network.domain; + +/** + * Orchestrates applying a user-entered DNS config, netplan-try style: + *
    + *
  1. validate (fail-closed) — invalid input is never saved;
  2. + *
  3. probe the server(s) for an actual DNS reply;
  4. + *
  5. if reachable, persist as custom; otherwise revert to defaults and report.
  6. + *
+ * Pure domain logic over the {@link DnsConfigRepository} and {@link DnsConnectivityProbe} + * ports — unit-testable with fakes. + */ +public final class ConfigureDnsUseCase { + + public enum Outcome { APPLIED, INVALID, UNREACHABLE } + + public static final class Result { + public final Outcome outcome; + public final String message; + private Result(Outcome outcome, String message) { this.outcome = outcome; this.message = message; } + static Result applied() { return new Result(Outcome.APPLIED, null); } + static Result invalid(String reason) { return new Result(Outcome.INVALID, reason); } + static Result unreachable() { return new Result(Outcome.UNREACHABLE, "DNS did not respond"); } + } + + private static final long DEFAULT_TIMEOUT_MS = 4000L; + + private final DnsConfigRepository repository; + private final DnsConnectivityProbe probe; + private final long timeoutMs; + + public ConfigureDnsUseCase(DnsConfigRepository repository, DnsConnectivityProbe probe) { + this(repository, probe, DEFAULT_TIMEOUT_MS); + } + + public ConfigureDnsUseCase(DnsConfigRepository repository, DnsConnectivityProbe probe, long timeoutMs) { + this.repository = repository; + this.probe = probe; + this.timeoutMs = timeoutMs; + } + + public Result execute(DnsConfig candidate) { + DnsValidator.Result validation = DnsValidator.validate(candidate); + if (!validation.valid) { + return Result.invalid(validation.reason); + } + boolean reachable = probe.isReachable(candidate.primary(), timeoutMs); + if (!reachable && candidate.hasSecondary()) { + reachable = probe.isReachable(candidate.secondary(), timeoutMs); + } + if (reachable) { + repository.saveCustom(candidate); + return Result.applied(); + } + repository.disableCustom(); + return Result.unreachable(); + } +} diff --git a/controller/app/src/main/java/org/iiab/controller/network/domain/DnsConnectivityProbe.java b/controller/app/src/main/java/org/iiab/controller/network/domain/DnsConnectivityProbe.java new file mode 100644 index 0000000..887c0bf --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/network/domain/DnsConnectivityProbe.java @@ -0,0 +1,24 @@ +/* + * ============================================================================ + * Name : DnsConnectivityProbe.java + * Author : AppDevForAll + * Copyright : Copyright (c) 2026 AppDevForAll + * Description : Domain port: can a DNS server actually answer (netplan-try style). + * ============================================================================ + */ +package org.iiab.controller.network.domain; + +/** + * Domain port that probes whether a DNS server is a working resolver reachable + * from the device. The Data layer implements it (a small UDP DNS query). Used for + * the netplan-try style verification before committing a user-chosen DNS. + */ +public interface DnsConnectivityProbe { + + /** + * @param dnsServer IPv4/IPv6 literal of the server to test + * @param timeoutMs how long to wait for a reply + * @return {@code true} if the server answered a DNS query within the timeout + */ + boolean isReachable(String dnsServer, long timeoutMs); +} diff --git a/controller/app/src/main/java/org/iiab/controller/network/presentation/DnsSettingsUiState.java b/controller/app/src/main/java/org/iiab/controller/network/presentation/DnsSettingsUiState.java new file mode 100644 index 0000000..698944a --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/network/presentation/DnsSettingsUiState.java @@ -0,0 +1,55 @@ +/* + * ============================================================================ + * Name : DnsSettingsUiState.java + * Author : AppDevForAll + * Copyright : Copyright (c) 2026 AppDevForAll + * Description : Immutable UI state for the Setup DNS panel. + * ============================================================================ + */ +package org.iiab.controller.network.presentation; + +/** + * Immutable UI state for the Setup DNS panel, exposed by {@link DnsSettingsViewModel}. + * {@code customEnabled} drives the checkbox; {@code primary}/{@code secondary} prefill + * the fields; {@code status} + {@code message} drive the result indicator. + */ +public final class DnsSettingsUiState { + + public enum Status { IDLE, TESTING, APPLIED, INVALID, UNREACHABLE } + + public final boolean customEnabled; + public final String primary; + public final String secondary; + public final Status status; + public final String message; + + private DnsSettingsUiState(boolean customEnabled, String primary, String secondary, Status status, String message) { + this.customEnabled = customEnabled; + this.primary = primary == null ? "" : primary; + this.secondary = secondary == null ? "" : secondary; + this.status = status; + this.message = message; + } + + public static DnsSettingsUiState idle(boolean customEnabled, String primary, String secondary) { + return new DnsSettingsUiState(customEnabled, primary, secondary, Status.IDLE, null); + } + + public static DnsSettingsUiState testing(String primary, String secondary) { + return new DnsSettingsUiState(true, primary, secondary, Status.TESTING, null); + } + + public static DnsSettingsUiState applied(String primary, String secondary) { + return new DnsSettingsUiState(true, primary, secondary, Status.APPLIED, null); + } + + public static DnsSettingsUiState invalid(String primary, String secondary, String message) { + return new DnsSettingsUiState(true, primary, secondary, Status.INVALID, message); + } + + /** Probe failed: the use case reverted to defaults, so the panel shows defaults again. */ + public static DnsSettingsUiState unreachable(String defaultPrimary, String defaultSecondary) { + return new DnsSettingsUiState(false, defaultPrimary, defaultSecondary, Status.UNREACHABLE, + "DNS did not respond — reverted to defaults"); + } +} diff --git a/controller/app/src/main/java/org/iiab/controller/network/presentation/DnsSettingsViewModel.java b/controller/app/src/main/java/org/iiab/controller/network/presentation/DnsSettingsViewModel.java new file mode 100644 index 0000000..4523c41 --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/network/presentation/DnsSettingsViewModel.java @@ -0,0 +1,89 @@ +/* + * ============================================================================ + * Name : DnsSettingsViewModel.java + * Author : AppDevForAll + * Copyright : Copyright (c) 2026 AppDevForAll + * Description : ViewModel for the Setup DNS panel; orchestrates validate/test/save off the main thread. + * ============================================================================ + */ +package org.iiab.controller.network.presentation; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import org.iiab.controller.network.domain.ConfigureDnsUseCase; +import org.iiab.controller.network.domain.DnsConfig; +import org.iiab.controller.network.domain.DnsConfigRepository; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Presentation-layer ViewModel for the Setup DNS panel. Observes nothing from the + * Data layer directly — only Domain abstractions. The Accept flow runs the + * {@link ConfigureDnsUseCase} (validate -> probe -> save/revert) off the main thread. + */ +public class DnsSettingsViewModel extends ViewModel { + + private final DnsConfigRepository repository; + private final ConfigureDnsUseCase configure; + private final ExecutorService executor = Executors.newSingleThreadExecutor(); + private final MutableLiveData state = new MutableLiveData<>(); + + public DnsSettingsViewModel(DnsConfigRepository repository, ConfigureDnsUseCase configure) { + this.repository = repository; + this.configure = configure; + load(); + } + + public LiveData state() { + return state; + } + + /** Initialise from persistence: checkbox reflects custom-enabled, fields prefilled. */ + public void load() { + DnsConfig prefill = repository.loadCustom(); + state.postValue(DnsSettingsUiState.idle(repository.isCustomEnabled(), prefill.primary(), prefill.secondary())); + } + + /** Checkbox toggled. Off = revert to defaults and clear the fields to the defaults. */ + public void onSetupToggled(boolean enabled) { + if (enabled) { + DnsConfig prefill = repository.loadCustom(); + state.postValue(DnsSettingsUiState.idle(true, prefill.primary(), prefill.secondary())); + } else { + executor.execute(() -> { + repository.disableCustom(); + DnsConfig d = DnsConfig.defaults(); + state.postValue(DnsSettingsUiState.idle(false, d.primary(), d.secondary())); + }); + } + } + + /** Accept/save: validate, netplan-try probe, then save or revert. */ + public void onAccept(String primary, String secondary) { + state.postValue(DnsSettingsUiState.testing(primary, secondary)); + executor.execute(() -> { + ConfigureDnsUseCase.Result result = configure.execute(new DnsConfig(primary, secondary)); + switch (result.outcome) { + case APPLIED: + state.postValue(DnsSettingsUiState.applied(primary, secondary)); + break; + case INVALID: + state.postValue(DnsSettingsUiState.invalid(primary, secondary, result.message)); + break; + case UNREACHABLE: + default: + DnsConfig d = DnsConfig.defaults(); + state.postValue(DnsSettingsUiState.unreachable(d.primary(), d.secondary())); + break; + } + }); + } + + @Override + protected void onCleared() { + executor.shutdownNow(); + } +} diff --git a/controller/app/src/main/java/org/iiab/controller/network/presentation/DnsSettingsViewModelFactory.java b/controller/app/src/main/java/org/iiab/controller/network/presentation/DnsSettingsViewModelFactory.java new file mode 100644 index 0000000..154a416 --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/network/presentation/DnsSettingsViewModelFactory.java @@ -0,0 +1,47 @@ +/* + * ============================================================================ + * Name : DnsSettingsViewModelFactory.java + * Author : AppDevForAll + * Copyright : Copyright (c) 2026 AppDevForAll + * Description : Manual DI wiring for DnsSettingsViewModel. + * ============================================================================ + */ +package org.iiab.controller.network.presentation; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.ViewModelProvider; + +import org.iiab.controller.network.data.PrefsDnsConfigRepository; +import org.iiab.controller.network.data.UdpDnsConnectivityProbe; +import org.iiab.controller.network.domain.ConfigureDnsUseCase; +import org.iiab.controller.network.domain.DnsConfigRepository; +import org.iiab.controller.network.domain.DnsConnectivityProbe; + +/** + * Manual Data -> Domain -> Presentation wiring for {@link DnsSettingsViewModel} + * (same hand-wired approach as {@code RootfsViewModelFactory}; no DI framework). + */ +public class DnsSettingsViewModelFactory implements ViewModelProvider.Factory { + + private final Context appContext; + + public DnsSettingsViewModelFactory(Context context) { + this.appContext = context.getApplicationContext(); + } + + @NonNull + @Override + @SuppressWarnings("unchecked") + public T create(@NonNull Class modelClass) { + if (modelClass.isAssignableFrom(DnsSettingsViewModel.class)) { + DnsConfigRepository repository = new PrefsDnsConfigRepository(appContext); + DnsConnectivityProbe probe = new UdpDnsConnectivityProbe(); + ConfigureDnsUseCase configure = new ConfigureDnsUseCase(repository, probe); + return (T) new DnsSettingsViewModel(repository, configure); + } + throw new IllegalArgumentException("Unknown ViewModel class: " + modelClass.getName()); + } +} diff --git a/controller/app/src/test/java/org/iiab/controller/network/domain/ConfigureDnsUseCaseTest.java b/controller/app/src/test/java/org/iiab/controller/network/domain/ConfigureDnsUseCaseTest.java new file mode 100644 index 0000000..ee09a79 --- /dev/null +++ b/controller/app/src/test/java/org/iiab/controller/network/domain/ConfigureDnsUseCaseTest.java @@ -0,0 +1,65 @@ +package org.iiab.controller.network.domain; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +/** validate -> probe -> save/revert, with fakes. Pure JVM. */ +public class ConfigureDnsUseCaseTest { + + private static final class FakeRepo implements DnsConfigRepository { + DnsConfig saved; + boolean enabled; + boolean disableCalled; + @Override public DnsConfig loadEffective() { return enabled && saved != null ? saved : DnsConfig.defaults(); } + @Override public DnsConfig loadCustom() { return saved != null ? saved : DnsConfig.defaults(); } + @Override public boolean isCustomEnabled() { return enabled; } + @Override public void saveCustom(DnsConfig c) { saved = c; enabled = true; } + @Override public void disableCustom() { enabled = false; disableCalled = true; } + } + + /** Reachable only for servers in the allow-set. */ + private static final class FakeProbe implements DnsConnectivityProbe { + private final java.util.Set reachable; + FakeProbe(String... ok) { reachable = new java.util.HashSet<>(java.util.Arrays.asList(ok)); } + @Override public boolean isReachable(String server, long timeoutMs) { return reachable.contains(server); } + } + + @Test public void appliedWhenValidAndReachable() { + FakeRepo repo = new FakeRepo(); + ConfigureDnsUseCase.Result r = + new ConfigureDnsUseCase(repo, new FakeProbe("1.1.1.1")).execute(new DnsConfig("1.1.1.1", "8.8.8.8")); + assertEquals(ConfigureDnsUseCase.Outcome.APPLIED, r.outcome); + assertEquals(new DnsConfig("1.1.1.1", "8.8.8.8"), repo.saved); + assertTrue(repo.enabled); + } + + @Test public void invalidIsNotSavedNorReverted() { + FakeRepo repo = new FakeRepo(); + ConfigureDnsUseCase.Result r = + new ConfigureDnsUseCase(repo, new FakeProbe("1.1.1.1")).execute(new DnsConfig("aaa.bbb.ccc", "")); + assertEquals(ConfigureDnsUseCase.Outcome.INVALID, r.outcome); + assertNull(repo.saved); + assertFalse(repo.disableCalled); + } + + @Test public void unreachableRevertsToDefaults() { + FakeRepo repo = new FakeRepo(); + ConfigureDnsUseCase.Result r = + new ConfigureDnsUseCase(repo, new FakeProbe()).execute(new DnsConfig("9.9.9.9", "")); + assertEquals(ConfigureDnsUseCase.Outcome.UNREACHABLE, r.outcome); + assertNull(repo.saved); + assertTrue(repo.disableCalled); + } + + @Test public void fallsBackToSecondaryWhenPrimaryUnreachable() { + FakeRepo repo = new FakeRepo(); + ConfigureDnsUseCase.Result r = + new ConfigureDnsUseCase(repo, new FakeProbe("8.8.8.8")).execute(new DnsConfig("9.9.9.9", "8.8.8.8")); + assertEquals(ConfigureDnsUseCase.Outcome.APPLIED, r.outcome); + assertTrue(repo.enabled); + } +} From c8e21a657340aa6711d67a2055288380b5a83de0 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Mon, 22 Jun 2026 18:01:28 +0000 Subject: [PATCH 2/4] feat(usage): Setup DNS panel wired to DnsSettingsViewModel (PR B, part 2/2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the two vestigial VPN-era DNS fields (dns_ipv4/dns_ipv6, a dead UI<->prefs loop) with the real Setup DNS UI in the existing 'advanced' collapsible: - fragment_usage.xml: 'Setup DNS' checkbox + a hidden dns_setup_fields block (Primary/Secondary inputs, Accept button, result line). Uses existing theme tokens. - UsageFragment: bind the views, obtain DnsSettingsViewModel via its factory, observe state, wire the checkbox (onSetupToggled — unchecking reverts + clears to defaults) and Accept (onAccept — validate + netplan-try probe + save/revert). renderDnsState reflects IDLE/TESTING/APPLIED/INVALID/UNREACHABLE. - strings: setup_dns, dns_primary, dns_secondary, dns_accept, dns_status_testing/ok. Removes only the two DNS fields (the direct fake predecessor of this feature). The broader VPN/SOCKS vestige (Preferences DNS keys, socks fields, dns_ipv4/ipv6 strings) stays for the cleanup PR. Builds on PR A + the PR B logic commit. --- .../org/iiab/controller/UsageFragment.java | 71 +++++++++++++++-- .../src/main/res/layout/fragment_usage.xml | 76 ++++++++++++++----- .../app/src/main/res/values/strings.xml | 6 ++ 3 files changed, 126 insertions(+), 27 deletions(-) diff --git a/controller/app/src/main/java/org/iiab/controller/UsageFragment.java b/controller/app/src/main/java/org/iiab/controller/UsageFragment.java index d7803de..17f8a4f 100644 --- a/controller/app/src/main/java/org/iiab/controller/UsageFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/UsageFragment.java @@ -32,6 +32,11 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; + +import org.iiab.controller.network.presentation.DnsSettingsUiState; +import org.iiab.controller.network.presentation.DnsSettingsViewModel; +import org.iiab.controller.network.presentation.DnsSettingsViewModelFactory; import com.google.android.material.snackbar.Snackbar; @@ -43,7 +48,7 @@ public class UsageFragment extends Fragment implements View.OnClickListener { private MainActivity mainActivity; // INTERFACE VARS - private EditText edittext_socks_addr, edittext_socks_udp_addr, edittext_socks_port, edittext_socks_user, edittext_socks_pass, edittext_dns_ipv4, edittext_dns_ipv6; + private EditText edittext_socks_addr, edittext_socks_udp_addr, edittext_socks_port, edittext_socks_user, edittext_socks_pass; private CheckBox checkbox_udp_in_tcp, checkbox_remote_dns, checkbox_global, checkbox_maintenance, checkbox_ipv4, checkbox_ipv6; private TextView textview_maintenance_warning, configLabel, advConfigLabel, logLabel, logWarning, logSizeText, connectionLog; private Button button_apps, button_save, button_control, button_browse_content, btnClearLog, btnCopyLog; @@ -53,6 +58,15 @@ public class UsageFragment extends Fragment implements View.OnClickListener { private DashboardManager dashboardManager; + // Setup DNS (network slice, PR B) + private CheckBox setup_dns_check; + private LinearLayout dns_setup_fields; + private EditText dns_primary, dns_secondary; + private Button dns_accept; + private TextView dns_result; + private DnsSettingsViewModel dnsViewModel; + private boolean suppressDnsToggle = false; + @Override public void onAttach(@NonNull Context context) { super.onAttach(context); @@ -78,8 +92,21 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat edittext_socks_port = view.findViewById(R.id.socks_port); edittext_socks_user = view.findViewById(R.id.socks_user); edittext_socks_pass = view.findViewById(R.id.socks_pass); - edittext_dns_ipv4 = view.findViewById(R.id.dns_ipv4); - edittext_dns_ipv6 = view.findViewById(R.id.dns_ipv6); + setup_dns_check = view.findViewById(R.id.setup_dns_check); + dns_setup_fields = view.findViewById(R.id.dns_setup_fields); + dns_primary = view.findViewById(R.id.dns_primary); + dns_secondary = view.findViewById(R.id.dns_secondary); + dns_accept = view.findViewById(R.id.dns_accept); + dns_result = view.findViewById(R.id.dns_result); + dnsViewModel = new ViewModelProvider(this, new DnsSettingsViewModelFactory(requireContext())) + .get(DnsSettingsViewModel.class); + dnsViewModel.state().observe(getViewLifecycleOwner(), this::renderDnsState); + setup_dns_check.setOnCheckedChangeListener((btn, checked) -> { + if (suppressDnsToggle) return; + dnsViewModel.onSetupToggled(checked); + }); + dns_accept.setOnClickListener(v -> dnsViewModel.onAccept( + dns_primary.getText().toString(), dns_secondary.getText().toString())); checkbox_ipv4 = view.findViewById(R.id.ipv4); checkbox_ipv6 = view.findViewById(R.id.ipv6); checkbox_global = view.findViewById(R.id.global); @@ -210,8 +237,6 @@ public void updateUI() { edittext_socks_port.setText(String.valueOf(mainActivity.prefs.getSocksPort())); edittext_socks_user.setText(mainActivity.prefs.getSocksUsername()); edittext_socks_pass.setText(mainActivity.prefs.getSocksPassword()); - edittext_dns_ipv4.setText(mainActivity.prefs.getDnsIpv4()); - edittext_dns_ipv6.setText(mainActivity.prefs.getDnsIpv6()); checkbox_ipv4.setChecked(mainActivity.prefs.getIpv4()); checkbox_ipv6.setChecked(mainActivity.prefs.getIpv6()); checkbox_global.setChecked(mainActivity.prefs.getGlobal()); @@ -450,10 +475,42 @@ public void savePrefsFromUI() { mainActivity.prefs.setRemoteDns(true); mainActivity.prefs.setGlobal(true); - mainActivity.prefs.setDnsIpv4(edittext_dns_ipv4.getText().toString()); - mainActivity.prefs.setDnsIpv6(edittext_dns_ipv6.getText().toString()); mainActivity.prefs.setMaintenanceMode(checkbox_maintenance.isChecked()); } + + private void renderDnsState(DnsSettingsUiState st) { + if (setup_dns_check == null) return; + suppressDnsToggle = true; + setup_dns_check.setChecked(st.customEnabled); + suppressDnsToggle = false; + dns_setup_fields.setVisibility(st.customEnabled ? View.VISIBLE : View.GONE); + if (st.status == DnsSettingsUiState.Status.IDLE || st.status == DnsSettingsUiState.Status.UNREACHABLE) { + dns_primary.setText(st.primary); + dns_secondary.setText(st.secondary); + } + switch (st.status) { + case TESTING: + dns_result.setVisibility(View.VISIBLE); + dns_result.setText(getString(R.string.dns_status_testing)); + dns_result.setTextColor(ContextCompat.getColor(requireContext(), R.color.text_secondary)); + break; + case APPLIED: + dns_result.setVisibility(View.VISIBLE); + dns_result.setText(getString(R.string.dns_status_ok)); + dns_result.setTextColor(ContextCompat.getColor(requireContext(), R.color.status_success)); + break; + case INVALID: + case UNREACHABLE: + dns_result.setVisibility(View.VISIBLE); + dns_result.setText(st.message != null ? st.message : ""); + dns_result.setTextColor(ContextCompat.getColor(requireContext(), R.color.status_warning)); + break; + default: + dns_result.setVisibility(View.GONE); + break; + } + } + public void highlightServerButton() { if (deckContainer == null || !isAdded()) return; diff --git a/controller/app/src/main/res/layout/fragment_usage.xml b/controller/app/src/main/res/layout/fragment_usage.xml index 9c7fcab..a686a4f 100644 --- a/controller/app/src/main/res/layout/fragment_usage.xml +++ b/controller/app/src/main/res/layout/fragment_usage.xml @@ -183,33 +183,69 @@ android:textColor="@color/text_on_accent" android:textAllCaps="false" /> - + android:layout_marginTop="8dp" + android:text="@string/setup_dns" /> - + android:orientation="vertical" + android:visibility="gone"> - + - + + + + + + +