Skip to content

Commit 1565dd8

Browse files
feat(network): restore DNS Setup UI (network-dns-slice part 2)
Re-lands the part-2 (PR B) work from #27, which was merged into the intermediate feat/network-dns-slice-part1 branch (not main) and lost when that branch was deleted. Brings the Setup DNS panel under a visible Advanced settings section in Usage, wired to the part-1 network slice. Ref: #27 (network-dns-slice part 2/2)
1 parent f56ad32 commit 1565dd8

16 files changed

Lines changed: 600 additions & 67 deletions

File tree

‎controller/app/src/main/java/org/iiab/controller/Preferences.java‎

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ public class Preferences {
2424
public static final String SOCKS_PORT = "SocksPort";
2525
public static final String SOCKS_USER = "SocksUser";
2626
public static final String SOCKS_PASS = "SocksPass";
27-
public static final String DNS_IPV4 = "DnsIpv4";
28-
public static final String DNS_IPV6 = "DnsIpv6";
2927
public static final String IPV4 = "Ipv4";
3028
public static final String IPV6 = "Ipv6";
3129
public static final String GLOBAL = "Global";
@@ -92,26 +90,6 @@ public void setSocksPassword(String pass) {
9290
editor.commit();
9391
}
9492

95-
public String getDnsIpv4() {
96-
return prefs.getString(DNS_IPV4, "8.8.8.8");
97-
}
98-
99-
public void setDnsIpv4(String addr) {
100-
SharedPreferences.Editor editor = prefs.edit();
101-
editor.putString(DNS_IPV4, addr);
102-
editor.commit();
103-
}
104-
105-
public String getDnsIpv6() {
106-
return prefs.getString(DNS_IPV6, "2001:4860:4860::8888");
107-
}
108-
109-
public void setDnsIpv6(String addr) {
110-
SharedPreferences.Editor editor = prefs.edit();
111-
editor.putString(DNS_IPV6, addr);
112-
editor.commit();
113-
}
114-
11593
public String getMappedDns() {
11694
return "198.18.0.2";
11795
}

‎controller/app/src/main/java/org/iiab/controller/UsageFragment.java‎

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@
3232
import androidx.annotation.NonNull;
3333
import androidx.annotation.Nullable;
3434
import androidx.fragment.app.Fragment;
35+
import androidx.lifecycle.ViewModelProvider;
36+
37+
import org.iiab.controller.network.presentation.DnsSettingsUiState;
38+
import org.iiab.controller.network.presentation.DnsSettingsViewModel;
39+
import org.iiab.controller.network.presentation.DnsSettingsViewModelFactory;
3540

3641
import com.google.android.material.snackbar.Snackbar;
3742

@@ -43,7 +48,7 @@ public class UsageFragment extends Fragment implements View.OnClickListener {
4348

4449
private MainActivity mainActivity;
4550
// INTERFACE VARS
46-
private EditText edittext_socks_addr, edittext_socks_udp_addr, edittext_socks_port, edittext_socks_user, edittext_socks_pass, edittext_dns_ipv4, edittext_dns_ipv6;
51+
private EditText edittext_socks_addr, edittext_socks_udp_addr, edittext_socks_port, edittext_socks_user, edittext_socks_pass;
4752
private CheckBox checkbox_udp_in_tcp, checkbox_remote_dns, checkbox_global, checkbox_maintenance, checkbox_ipv4, checkbox_ipv6;
4853
private TextView textview_maintenance_warning, configLabel, advConfigLabel, logLabel, logWarning, logSizeText, connectionLog;
4954
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 {
5358

5459
private DashboardManager dashboardManager;
5560

61+
// Setup DNS (network slice, PR B)
62+
private CheckBox setup_dns_check;
63+
private LinearLayout dns_setup_fields;
64+
private EditText dns_primary, dns_secondary;
65+
private Button dns_accept;
66+
private TextView dns_result;
67+
private TextView dns_settings_label;
68+
private LinearLayout dns_settings_section;
69+
private DnsSettingsViewModel dnsViewModel;
70+
private boolean suppressDnsToggle = false;
71+
5672
@Override
5773
public void onAttach(@NonNull Context context) {
5874
super.onAttach(context);
@@ -78,8 +94,25 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
7894
edittext_socks_port = view.findViewById(R.id.socks_port);
7995
edittext_socks_user = view.findViewById(R.id.socks_user);
8096
edittext_socks_pass = view.findViewById(R.id.socks_pass);
81-
edittext_dns_ipv4 = view.findViewById(R.id.dns_ipv4);
82-
edittext_dns_ipv6 = view.findViewById(R.id.dns_ipv6);
97+
setup_dns_check = view.findViewById(R.id.setup_dns_check);
98+
dns_setup_fields = view.findViewById(R.id.dns_setup_fields);
99+
dns_primary = view.findViewById(R.id.dns_primary);
100+
dns_secondary = view.findViewById(R.id.dns_secondary);
101+
dns_accept = view.findViewById(R.id.dns_accept);
102+
dns_result = view.findViewById(R.id.dns_result);
103+
dns_settings_label = view.findViewById(R.id.dns_settings_label);
104+
dns_settings_section = view.findViewById(R.id.dns_settings_section);
105+
dns_settings_label.setText(String.format(getString(R.string.label_separator_up), getString(R.string.network_advanced_label)));
106+
dns_settings_label.setOnClickListener(v -> toggleVisibility(dns_settings_section, dns_settings_label, getString(R.string.network_advanced_label)));
107+
dnsViewModel = new ViewModelProvider(this, new DnsSettingsViewModelFactory(requireContext()))
108+
.get(DnsSettingsViewModel.class);
109+
dnsViewModel.state().observe(getViewLifecycleOwner(), this::renderDnsState);
110+
setup_dns_check.setOnCheckedChangeListener((btn, checked) -> {
111+
if (suppressDnsToggle) return;
112+
dnsViewModel.onSetupToggled(checked);
113+
});
114+
dns_accept.setOnClickListener(v -> dnsViewModel.onAccept(
115+
dns_primary.getText().toString(), dns_secondary.getText().toString()));
83116
checkbox_ipv4 = view.findViewById(R.id.ipv4);
84117
checkbox_ipv6 = view.findViewById(R.id.ipv6);
85118
checkbox_global = view.findViewById(R.id.global);
@@ -210,8 +243,6 @@ public void updateUI() {
210243
edittext_socks_port.setText(String.valueOf(mainActivity.prefs.getSocksPort()));
211244
edittext_socks_user.setText(mainActivity.prefs.getSocksUsername());
212245
edittext_socks_pass.setText(mainActivity.prefs.getSocksPassword());
213-
edittext_dns_ipv4.setText(mainActivity.prefs.getDnsIpv4());
214-
edittext_dns_ipv6.setText(mainActivity.prefs.getDnsIpv6());
215246
checkbox_ipv4.setChecked(mainActivity.prefs.getIpv4());
216247
checkbox_ipv6.setChecked(mainActivity.prefs.getIpv6());
217248
checkbox_global.setChecked(mainActivity.prefs.getGlobal());
@@ -450,10 +481,42 @@ public void savePrefsFromUI() {
450481
mainActivity.prefs.setRemoteDns(true);
451482
mainActivity.prefs.setGlobal(true);
452483

453-
mainActivity.prefs.setDnsIpv4(edittext_dns_ipv4.getText().toString());
454-
mainActivity.prefs.setDnsIpv6(edittext_dns_ipv6.getText().toString());
455484
mainActivity.prefs.setMaintenanceMode(checkbox_maintenance.isChecked());
456485
}
486+
487+
private void renderDnsState(DnsSettingsUiState st) {
488+
if (setup_dns_check == null) return;
489+
suppressDnsToggle = true;
490+
setup_dns_check.setChecked(st.customEnabled);
491+
suppressDnsToggle = false;
492+
dns_setup_fields.setVisibility(st.customEnabled ? View.VISIBLE : View.GONE);
493+
if (st.status == DnsSettingsUiState.Status.IDLE || st.status == DnsSettingsUiState.Status.UNREACHABLE) {
494+
dns_primary.setText(st.primary);
495+
dns_secondary.setText(st.secondary);
496+
}
497+
switch (st.status) {
498+
case TESTING:
499+
dns_result.setVisibility(View.VISIBLE);
500+
dns_result.setText(getString(R.string.dns_status_testing));
501+
dns_result.setTextColor(ContextCompat.getColor(requireContext(), R.color.text_secondary));
502+
break;
503+
case APPLIED:
504+
dns_result.setVisibility(View.VISIBLE);
505+
dns_result.setText(getString(R.string.dns_status_ok));
506+
dns_result.setTextColor(ContextCompat.getColor(requireContext(), R.color.status_success));
507+
break;
508+
case INVALID:
509+
case UNREACHABLE:
510+
dns_result.setVisibility(View.VISIBLE);
511+
dns_result.setText(st.message != null ? st.message : "");
512+
dns_result.setTextColor(ContextCompat.getColor(requireContext(), R.color.status_warning));
513+
break;
514+
default:
515+
dns_result.setVisibility(View.GONE);
516+
break;
517+
}
518+
}
519+
457520
public void highlightServerButton() {
458521
if (deckContainer == null || !isAdded()) return;
459522

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* ============================================================================
3+
* Name : UdpDnsConnectivityProbe.java
4+
* Author : AppDevForAll
5+
* Copyright : Copyright (c) 2026 AppDevForAll
6+
* Description : UDP DNS-query probe: sends a query to the server and waits for a reply.
7+
* ============================================================================
8+
*/
9+
package org.iiab.controller.network.data;
10+
11+
import android.util.Log;
12+
13+
import org.iiab.controller.network.domain.DnsConnectivityProbe;
14+
15+
import java.io.ByteArrayOutputStream;
16+
import java.net.DatagramPacket;
17+
import java.net.DatagramSocket;
18+
import java.net.InetAddress;
19+
import java.nio.charset.StandardCharsets;
20+
21+
/**
22+
* {@link DnsConnectivityProbe} that sends a tiny DNS A-query (for a fixed, stable
23+
* name) over UDP to the given server on port 53 and waits for a valid reply.
24+
* Pure {@code java.net}; works for IPv4 and IPv6 servers. Never throws — any
25+
* error (timeout, unreachable, malformed) means "not reachable".
26+
*/
27+
public final class UdpDnsConnectivityProbe implements DnsConnectivityProbe {
28+
29+
private static final String TAG = "IIAB-DNS";
30+
private static final String PROBE_NAME = "example.com";
31+
private static final int DNS_PORT = 53;
32+
33+
@Override
34+
public boolean isReachable(String dnsServer, long timeoutMs) {
35+
if (dnsServer == null || dnsServer.trim().isEmpty()) return false;
36+
DatagramSocket socket = null;
37+
try {
38+
byte[] query = buildQuery(PROBE_NAME);
39+
InetAddress addr = InetAddress.getByName(dnsServer.trim());
40+
socket = new DatagramSocket();
41+
socket.setSoTimeout((int) Math.max(1, Math.min(timeoutMs, Integer.MAX_VALUE)));
42+
socket.send(new DatagramPacket(query, query.length, addr, DNS_PORT));
43+
44+
byte[] buf = new byte[512];
45+
DatagramPacket response = new DatagramPacket(buf, buf.length);
46+
socket.receive(response);
47+
48+
if (response.getLength() < 12) return false;
49+
boolean idMatches = buf[0] == query[0] && buf[1] == query[1];
50+
boolean isResponse = (buf[2] & 0x80) != 0;
51+
int rcode = buf[3] & 0x0F;
52+
return idMatches && isResponse && rcode == 0;
53+
} catch (Exception e) {
54+
Log.i(TAG, "DNS probe to " + dnsServer + " failed: " + e.getMessage());
55+
return false;
56+
} finally {
57+
if (socket != null) socket.close();
58+
}
59+
}
60+
61+
private static byte[] buildQuery(String name) {
62+
ByteArrayOutputStream out = new ByteArrayOutputStream();
63+
int id = (int) (System.nanoTime() & 0xFFFF);
64+
out.write((id >> 8) & 0xFF);
65+
out.write(id & 0xFF);
66+
out.write(0x01);
67+
out.write(0x00);
68+
out.write(0x00);
69+
out.write(0x01);
70+
out.write(0x00);
71+
out.write(0x00);
72+
out.write(0x00);
73+
out.write(0x00);
74+
out.write(0x00);
75+
out.write(0x00);
76+
for (String label : name.split("\\.")) {
77+
byte[] b = label.getBytes(StandardCharsets.US_ASCII);
78+
out.write(b.length);
79+
out.write(b, 0, b.length);
80+
}
81+
out.write(0x00);
82+
out.write(0x00);
83+
out.write(0x01);
84+
out.write(0x00);
85+
out.write(0x01);
86+
return out.toByteArray();
87+
}
88+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* ============================================================================
3+
* Name : ConfigureDnsUseCase.java
4+
* Author : AppDevForAll
5+
* Copyright : Copyright (c) 2026 AppDevForAll
6+
* Description : Validate, netplan-try probe, then save (or revert to defaults).
7+
* ============================================================================
8+
*/
9+
package org.iiab.controller.network.domain;
10+
11+
/**
12+
* Orchestrates applying a user-entered DNS config, netplan-try style:
13+
* <ol>
14+
* <li>validate (fail-closed) — invalid input is never saved;</li>
15+
* <li>probe the server(s) for an actual DNS reply;</li>
16+
* <li>if reachable, persist as custom; otherwise revert to defaults and report.</li>
17+
* </ol>
18+
* Pure domain logic over the {@link DnsConfigRepository} and {@link DnsConnectivityProbe}
19+
* ports — unit-testable with fakes.
20+
*/
21+
public final class ConfigureDnsUseCase {
22+
23+
public enum Outcome { APPLIED, INVALID, UNREACHABLE }
24+
25+
public static final class Result {
26+
public final Outcome outcome;
27+
public final String message;
28+
private Result(Outcome outcome, String message) { this.outcome = outcome; this.message = message; }
29+
static Result applied() { return new Result(Outcome.APPLIED, null); }
30+
static Result invalid(String reason) { return new Result(Outcome.INVALID, reason); }
31+
static Result unreachable() { return new Result(Outcome.UNREACHABLE, "DNS did not respond"); }
32+
}
33+
34+
private static final long DEFAULT_TIMEOUT_MS = 4000L;
35+
36+
private final DnsConfigRepository repository;
37+
private final DnsConnectivityProbe probe;
38+
private final long timeoutMs;
39+
40+
public ConfigureDnsUseCase(DnsConfigRepository repository, DnsConnectivityProbe probe) {
41+
this(repository, probe, DEFAULT_TIMEOUT_MS);
42+
}
43+
44+
public ConfigureDnsUseCase(DnsConfigRepository repository, DnsConnectivityProbe probe, long timeoutMs) {
45+
this.repository = repository;
46+
this.probe = probe;
47+
this.timeoutMs = timeoutMs;
48+
}
49+
50+
public Result execute(DnsConfig candidate) {
51+
DnsValidator.Result validation = DnsValidator.validate(candidate);
52+
if (!validation.valid) {
53+
return Result.invalid(validation.reason);
54+
}
55+
boolean reachable = probe.isReachable(candidate.primary(), timeoutMs);
56+
if (!reachable && candidate.hasSecondary()) {
57+
reachable = probe.isReachable(candidate.secondary(), timeoutMs);
58+
}
59+
if (reachable) {
60+
repository.saveCustom(candidate);
61+
return Result.applied();
62+
}
63+
repository.disableCustom();
64+
return Result.unreachable();
65+
}
66+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* ============================================================================
3+
* Name : DnsConnectivityProbe.java
4+
* Author : AppDevForAll
5+
* Copyright : Copyright (c) 2026 AppDevForAll
6+
* Description : Domain port: can a DNS server actually answer (netplan-try style).
7+
* ============================================================================
8+
*/
9+
package org.iiab.controller.network.domain;
10+
11+
/**
12+
* Domain port that probes whether a DNS server is a working resolver reachable
13+
* from the device. The Data layer implements it (a small UDP DNS query). Used for
14+
* the netplan-try style verification before committing a user-chosen DNS.
15+
*/
16+
public interface DnsConnectivityProbe {
17+
18+
/**
19+
* @param dnsServer IPv4/IPv6 literal of the server to test
20+
* @param timeoutMs how long to wait for a reply
21+
* @return {@code true} if the server answered a DNS query within the timeout
22+
*/
23+
boolean isReachable(String dnsServer, long timeoutMs);
24+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* ============================================================================
3+
* Name : DnsSettingsUiState.java
4+
* Author : AppDevForAll
5+
* Copyright : Copyright (c) 2026 AppDevForAll
6+
* Description : Immutable UI state for the Setup DNS panel.
7+
* ============================================================================
8+
*/
9+
package org.iiab.controller.network.presentation;
10+
11+
/**
12+
* Immutable UI state for the Setup DNS panel, exposed by {@link DnsSettingsViewModel}.
13+
* {@code customEnabled} drives the checkbox; {@code primary}/{@code secondary} prefill
14+
* the fields; {@code status} + {@code message} drive the result indicator.
15+
*/
16+
public final class DnsSettingsUiState {
17+
18+
public enum Status { IDLE, TESTING, APPLIED, INVALID, UNREACHABLE }
19+
20+
public final boolean customEnabled;
21+
public final String primary;
22+
public final String secondary;
23+
public final Status status;
24+
public final String message;
25+
26+
private DnsSettingsUiState(boolean customEnabled, String primary, String secondary, Status status, String message) {
27+
this.customEnabled = customEnabled;
28+
this.primary = primary == null ? "" : primary;
29+
this.secondary = secondary == null ? "" : secondary;
30+
this.status = status;
31+
this.message = message;
32+
}
33+
34+
public static DnsSettingsUiState idle(boolean customEnabled, String primary, String secondary) {
35+
return new DnsSettingsUiState(customEnabled, primary, secondary, Status.IDLE, null);
36+
}
37+
38+
public static DnsSettingsUiState testing(String primary, String secondary) {
39+
return new DnsSettingsUiState(true, primary, secondary, Status.TESTING, null);
40+
}
41+
42+
public static DnsSettingsUiState applied(String primary, String secondary) {
43+
return new DnsSettingsUiState(true, primary, secondary, Status.APPLIED, null);
44+
}
45+
46+
public static DnsSettingsUiState invalid(String primary, String secondary, String message) {
47+
return new DnsSettingsUiState(true, primary, secondary, Status.INVALID, message);
48+
}
49+
50+
/** Probe failed: the use case reverted to defaults, so the panel shows defaults again. */
51+
public static DnsSettingsUiState unreachable(String defaultPrimary, String defaultSecondary) {
52+
return new DnsSettingsUiState(false, defaultPrimary, defaultSecondary, Status.UNREACHABLE,
53+
"DNS did not respond — reverted to defaults");
54+
}
55+
}

0 commit comments

Comments
 (0)