diff --git a/controller/app/src/main/java/org/iiab/controller/Preferences.java b/controller/app/src/main/java/org/iiab/controller/Preferences.java
index 53454fc..f94d178 100644
--- a/controller/app/src/main/java/org/iiab/controller/Preferences.java
+++ b/controller/app/src/main/java/org/iiab/controller/Preferences.java
@@ -24,8 +24,6 @@ public class Preferences {
public static final String SOCKS_PORT = "SocksPort";
public static final String SOCKS_USER = "SocksUser";
public static final String SOCKS_PASS = "SocksPass";
- public static final String DNS_IPV4 = "DnsIpv4";
- public static final String DNS_IPV6 = "DnsIpv6";
public static final String IPV4 = "Ipv4";
public static final String IPV6 = "Ipv6";
public static final String GLOBAL = "Global";
@@ -92,26 +90,6 @@ public void setSocksPassword(String pass) {
editor.commit();
}
- public String getDnsIpv4() {
- return prefs.getString(DNS_IPV4, "8.8.8.8");
- }
-
- public void setDnsIpv4(String addr) {
- SharedPreferences.Editor editor = prefs.edit();
- editor.putString(DNS_IPV4, addr);
- editor.commit();
- }
-
- public String getDnsIpv6() {
- return prefs.getString(DNS_IPV6, "2001:4860:4860::8888");
- }
-
- public void setDnsIpv6(String addr) {
- SharedPreferences.Editor editor = prefs.edit();
- editor.putString(DNS_IPV6, addr);
- editor.commit();
- }
-
public String getMappedDns() {
return "198.18.0.2";
}
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..832e7d2 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,17 @@ 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 TextView dns_settings_label;
+ private LinearLayout dns_settings_section;
+ private DnsSettingsViewModel dnsViewModel;
+ private boolean suppressDnsToggle = false;
+
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
@@ -78,8 +94,25 @@ 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);
+ dns_settings_label = view.findViewById(R.id.dns_settings_label);
+ dns_settings_section = view.findViewById(R.id.dns_settings_section);
+ dns_settings_label.setText(String.format(getString(R.string.label_separator_up), getString(R.string.network_advanced_label)));
+ dns_settings_label.setOnClickListener(v -> toggleVisibility(dns_settings_section, dns_settings_label, getString(R.string.network_advanced_label)));
+ 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 +243,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 +481,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/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:
+ *
+ * - validate (fail-closed) — invalid input is never saved;
+ * - probe the server(s) for an actual DNS reply;
+ * - if reachable, persist as custom; otherwise revert to defaults and report.
+ *
+ * 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/main/res/layout/fragment_usage.xml b/controller/app/src/main/res/layout/fragment_usage.xml
index 9c7fcab..d9678e5 100644
--- a/controller/app/src/main/res/layout/fragment_usage.xml
+++ b/controller/app/src/main/res/layout/fragment_usage.xml
@@ -183,33 +183,7 @@
android:textColor="@color/text_on_accent"
android:textAllCaps="false" />
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Desactivar Safe Pocket Web
Activar Safe Pocket Web
- DNS IPv4:
- DNS IPv6:
Global
IPv4
IPv6
diff --git a/controller/app/src/main/res/values-fr/strings.xml b/controller/app/src/main/res/values-fr/strings.xml
index 7950569..e378f50 100644
--- a/controller/app/src/main/res/values-fr/strings.xml
+++ b/controller/app/src/main/res/values-fr/strings.xml
@@ -107,8 +107,6 @@
Désactiver Safe Pocket Web
Activer Safe Pocket Web
- DNS IPv4 :
- DNS IPv6 :
Global
IPv4
IPv6
diff --git a/controller/app/src/main/res/values-hi/strings.xml b/controller/app/src/main/res/values-hi/strings.xml
index eb0f935..cd3279a 100644
--- a/controller/app/src/main/res/values-hi/strings.xml
+++ b/controller/app/src/main/res/values-hi/strings.xml
@@ -107,8 +107,6 @@
Safe Pocket Web अक्षम करें
Safe Pocket Web सक्षम करें
- DNS IPv4:
- DNS IPv6:
ग्लोबल
IPv4
IPv6
diff --git a/controller/app/src/main/res/values-pt/strings.xml b/controller/app/src/main/res/values-pt/strings.xml
index 015cf9f..68002e7 100644
--- a/controller/app/src/main/res/values-pt/strings.xml
+++ b/controller/app/src/main/res/values-pt/strings.xml
@@ -107,8 +107,6 @@
Desativar Safe Pocket Web
Ativar Safe Pocket Web
- DNS IPv4:
- DNS IPv6:
Global
IPv4
IPv6
diff --git a/controller/app/src/main/res/values-ru-rRU/strings.xml b/controller/app/src/main/res/values-ru-rRU/strings.xml
index a2d39ee..4c95740 100644
--- a/controller/app/src/main/res/values-ru-rRU/strings.xml
+++ b/controller/app/src/main/res/values-ru-rRU/strings.xml
@@ -107,8 +107,6 @@
Отключить Safe Pocket Web
Включить Safe Pocket Web
- DNS IPv4:
- DNS IPv6:
Глобально
IPv4
IPv6
diff --git a/controller/app/src/main/res/values/strings.xml b/controller/app/src/main/res/values/strings.xml
index bef9101..59bdc09 100644
--- a/controller/app/src/main/res/values/strings.xml
+++ b/controller/app/src/main/res/values/strings.xml
@@ -109,8 +109,13 @@
Disable Safe Pocket Web
Enable Safe Pocket Web
- DNS IPv4:
- DNS IPv6:
+ Setup DNS
+ Advanced settings
+ Primary DNS
+ Secondary DNS
+ Accept & save
+ Testing…
+ DNS works — saved
Global
IPv4
IPv6
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);
+ }
+}