This commit is contained in:
2026-08-16 13:17:19 -03:00
parent c0a337be3f
commit 7b8e7bfbd0
82 changed files with 5479 additions and 1016 deletions
@@ -1,44 +0,0 @@
package com.dragontcp.client;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public final class AppLog {
public interface Listener { void onLine(String line); }
private static final int MAX_LINES = 600;
private static final ArrayDeque<String> lines = new ArrayDeque<>();
private static final CopyOnWriteArrayList<Listener> listeners = new CopyOnWriteArrayList<>();
private AppLog() {}
public static void append(String line) {
if (line == null) return;
line = line.trim();
if (line.isEmpty()) return;
synchronized (lines) {
while (lines.size() >= MAX_LINES) lines.removeFirst();
lines.addLast(line);
}
for (Listener listener : listeners) {
try { listener.onLine(line); } catch (Throwable ignored) {}
}
}
public static String history() {
StringBuilder out = new StringBuilder();
synchronized (lines) {
for (String line : lines) out.append(line).append('\n');
}
return out.toString();
}
public static void clear() {
synchronized (lines) { lines.clear(); }
}
public static void addListener(Listener listener) { listeners.addIfAbsent(listener); }
public static void removeListener(Listener listener) { listeners.remove(listener); }
}
@@ -1,417 +0,0 @@
package com.dragontcp.client;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.VpnService;
import android.os.Build;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import tech.xvanturing.freeproxy.data.model.DnsMode;
import tech.xvanturing.freeproxy.data.model.ProxyProfile;
import tech.xvanturing.freeproxy.data.model.ProxyType;
import tech.xvanturing.freeproxy.vpn.TunnelEngine;
import tech.xvanturing.freeproxy.vpn.proxy.SocketProtector;
public class DragonService extends VpnService {
public static final String ACTION_CONNECT = "com.dragontcp.client.CONNECT";
public static final String ACTION_STOP = "com.dragontcp.client.STOP";
public static final String ACTION_STATE = "com.dragontcp.client.STATE";
public static final String EXTRA_ACTIVE = "active";
public static final String EXTRA_STATUS = "status";
public static final String EXTRA_SERVER = "server";
public static final String EXTRA_PORT = "port";
public static final String EXTRA_TOKEN = "token";
public static final String EXTRA_CHUNK_MAX = "chunkMax";
public static final String EXTRA_CHUNK_MIN = "chunkMin";
public static final String EXTRA_RECONNECT = "reconnect";
public static final String EXTRA_TIMEOUT = "timeout";
private static final int NOTIFICATION_ID = 53;
private static final String CHANNEL_ID = "dragontcp-lite";
private static final int LOCAL_PROXY_PORT = 8080;
private static final int TUN_MTU = 1400;
private static volatile boolean appActive;
private static volatile String appStatus = "DISCONNECTED";
public static boolean isActive() { return appActive; }
public static String currentStatus() { return appStatus; }
private final Object stateLock = new Object();
private volatile Process coreProcess;
private volatile TunnelEngine tunnelEngine;
private volatile ParcelFileDescriptor tunFd;
private volatile boolean connected;
private volatile boolean stopping;
private volatile boolean cancelRequested;
@Override
public void onCreate() {
super.onCreate();
createNotificationChannel();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
String action = intent != null ? intent.getAction() : null;
if (ACTION_STOP.equals(action)) {
cancelRequested = true;
publishState("STOPPING", true);
new Thread(() -> stopEverything("Stopped"), "dragontcp-stop").start();
return START_NOT_STICKY;
}
if (ACTION_CONNECT.equals(action)) {
synchronized (stateLock) {
// UI disabling is backed by a service-side guard too. A duplicate
// CONNECT can no longer tear down and restart a healthy tunnel.
if (appActive || connected || coreProcess != null || tunnelEngine != null) {
publishState(connected ? "CONNECTED" : "CONNECTING", true);
return START_STICKY;
}
cancelRequested = false;
publishState("CONNECTING", true);
}
startForeground(NOTIFICATION_ID, buildNotification("Starting..."));
Intent copy = new Intent(intent);
new Thread(() -> startEverything(copy), "dragontcp-start").start();
return START_STICKY;
}
return START_NOT_STICKY;
}
private void startEverything(Intent intent) {
synchronized (stateLock) {
stopping = false;
}
String server = intent.getStringExtra(EXTRA_SERVER);
int port = intent.getIntExtra(EXTRA_PORT, 53);
String token = intent.getStringExtra(EXTRA_TOKEN);
int chunkMax = intent.getIntExtra(EXTRA_CHUNK_MAX, 1024 * 1024);
int chunkMin = intent.getIntExtra(EXTRA_CHUNK_MIN, 32);
int reconnect = intent.getIntExtra(EXTRA_RECONNECT, 1);
int timeout = intent.getIntExtra(EXTRA_TIMEOUT, 2);
if (server == null || server.trim().isEmpty()) {
failStart("Server is required");
return;
}
server = server.trim();
if (token == null) token = "";
chunkMax = Math.max(32, Math.min(1024 * 1024, chunkMax));
chunkMin = Math.max(32, Math.min(chunkMax, chunkMin));
reconnect = Math.max(1, reconnect);
timeout = Math.max(1, timeout);
try {
AppLog.append("Starting DragonTCP → " + server + ":" + port);
Process process = startDragonCore(server, port, token, chunkMax, chunkMin, reconnect, timeout);
synchronized (stateLock) { coreProcess = process; }
startCoreLogReader(process);
waitForLocalProxy(process);
if (cancelRequested) throw new InterruptedException("Stop requested");
AppLog.append("Local DragonTCP proxy ready on 127.0.0.1:8080");
ParcelFileDescriptor pfd = establishVpn();
if (pfd == null) throw new IllegalStateException("Android refused to establish the VPN interface");
SocketProtector protector = new SocketProtector() {
@Override public boolean protect(Socket socket) {
return DragonService.this.protect(socket);
}
@Override public boolean protect(DatagramSocket socket) {
return DragonService.this.protect(socket);
}
};
ProxyProfile profile = new ProxyProfile(ProxyType.HTTP, DnsMode.PROXY, false);
InetSocketAddress localProxy = new InetSocketAddress(
InetAddress.getByAddress(new byte[]{127, 0, 0, 1}),
LOCAL_PROXY_PORT
);
TunnelEngine engine = new TunnelEngine(
pfd,
profile,
localProxy,
TUN_MTU,
protector,
null,
null
);
synchronized (stateLock) {
if (cancelRequested) {
try { pfd.close(); } catch (Throwable ignored) {}
throw new InterruptedException("Stop requested");
}
tunFd = pfd;
tunnelEngine = engine;
connected = true;
}
engine.start();
publishState("CONNECTED", true);
updateNotification("Connected");
AppLog.append("VPN connected");
AppLog.append("DNS: forced through 1.1.1.1 over DragonTCP");
AppLog.append("IPv6: captured and blocked to prevent bypass");
} catch (InterruptedException stopped) {
Thread.currentThread().interrupt();
stopEverything(null);
} catch (Throwable t) {
failStart(t.getMessage() != null ? t.getMessage() : t.toString());
}
}
private Process startDragonCore(
String server,
int port,
String token,
int chunkMax,
int chunkMin,
int reconnect,
int timeout
) throws Exception {
File executable = new File(getApplicationInfo().nativeLibraryDir, "libdragontcp_client.so");
if (!executable.exists()) throw new IllegalStateException("Embedded DragonTCP core is missing");
List<String> cmd = new ArrayList<>();
cmd.add(executable.getAbsolutePath());
cmd.add("--listen-host"); cmd.add("127.0.0.1");
cmd.add("--listen-port"); cmd.add(Integer.toString(LOCAL_PROXY_PORT));
cmd.add("--server-host"); cmd.add(server);
cmd.add("--server-port"); cmd.add(Integer.toString(port));
if (!token.isEmpty()) { cmd.add("--token"); cmd.add(token); }
cmd.add("--transport"); cmd.add("chunk");
cmd.add("--chunk-start"); cmd.add(Integer.toString(chunkMax));
cmd.add("--chunk-min"); cmd.add(Integer.toString(chunkMin));
cmd.add("--chunk-max"); cmd.add(Integer.toString(chunkMax));
cmd.add("--chunk-pollers"); cmd.add("1");
cmd.add("--chunk-reconnect-every"); cmd.add(Integer.toString(reconnect));
cmd.add("--chunk-timeout"); cmd.add(timeout + "s");
cmd.add("--chunk-grow-after"); cmd.add("16");
cmd.add("--chunk-adapt-log=true");
ProcessBuilder pb = new ProcessBuilder(cmd);
pb.redirectErrorStream(true);
return pb.start();
}
private void startCoreLogReader(Process process) {
Thread reader = new Thread(() -> {
try (BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = br.readLine()) != null) {
// Keep the UI useful: adaptation changes and real errors only.
String lower = line.toLowerCase();
if (line.startsWith("adaptive ") || lower.contains("error") || lower.contains("failed")) {
AppLog.append(line);
}
}
} catch (Throwable ignored) {
}
}, "dragontcp-core-log");
reader.setDaemon(true);
reader.start();
Thread watcher = new Thread(() -> {
try {
int code = process.waitFor();
boolean shouldStop;
synchronized (stateLock) {
shouldStop = !stopping && coreProcess == process && connected;
}
if (shouldStop) {
AppLog.append("DragonTCP core exited: " + code);
stopEverything("Core stopped");
}
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
}, "dragontcp-core-watch");
watcher.setDaemon(true);
watcher.start();
}
private void waitForLocalProxy(Process process) throws Exception {
long deadline = System.currentTimeMillis() + 10_000;
Throwable last = null;
while (System.currentTimeMillis() < deadline) {
if (!process.isAlive()) throw new IllegalStateException("DragonTCP core exited before proxy startup");
try (Socket socket = new Socket()) {
socket.connect(new InetSocketAddress("127.0.0.1", LOCAL_PROXY_PORT), 150);
return;
} catch (Throwable t) {
last = t;
Thread.sleep(100);
}
}
throw new IllegalStateException("Local proxy did not start" + (last != null ? ": " + last.getMessage() : ""));
}
private ParcelFileDescriptor establishVpn() throws Exception {
Builder builder = new Builder()
.setSession("DragonTCP Lite")
.setMtu(TUN_MTU)
.addAddress("10.77.0.2", 32)
.addRoute("0.0.0.0", 0)
.addDnsServer("1.1.1.1")
// The embedded userspace adapter is intentionally IPv4-only.
// Capturing ::/0 blocks IPv6 instead of leaking it outside the VPN.
.addAddress("fd77:6472:6167:6f6e::2", 128)
.addRoute("::", 0);
try {
builder.addDisallowedApplication(getPackageName());
} catch (PackageManager.NameNotFoundException ignored) {
}
if (Build.VERSION.SDK_INT >= 29) {
builder.setBlocking(true);
builder.setMetered(false);
}
return builder.establish();
}
private void failStart(String message) {
AppLog.append("CONNECT failed: " + message);
stopEverything("Failed");
}
private void stopEverything(String logMessage) {
synchronized (stateLock) {
stopEverythingLocked(logMessage);
}
}
private void cleanupComponentsLocked() {
connected = false;
TunnelEngine engine = tunnelEngine;
tunnelEngine = null;
if (engine != null) {
try { engine.stop(); } catch (Throwable ignored) {}
}
ParcelFileDescriptor fd = tunFd;
tunFd = null;
if (fd != null) {
try { fd.close(); } catch (Throwable ignored) {}
}
Process process = coreProcess;
coreProcess = null;
if (process != null) {
try {
process.destroy();
if (!process.waitFor(1200, TimeUnit.MILLISECONDS)) {
process.destroyForcibly();
process.waitFor(800, TimeUnit.MILLISECONDS);
}
} catch (Throwable ignored) {
try { process.destroyForcibly(); } catch (Throwable ignored2) {}
}
}
}
private void stopEverythingLocked(String logMessage) {
if (stopping) return;
stopping = true;
cleanupComponentsLocked();
if (logMessage != null) AppLog.append(logMessage);
publishState("DISCONNECTED", false);
try { stopForeground(true); } catch (Throwable ignored) {}
stopSelf();
stopping = false;
}
@Override
public void onRevoke() {
stopEverything("VPN permission revoked");
super.onRevoke();
}
@Override
public void onDestroy() {
stopEverything(null);
super.onDestroy();
}
@Override
public IBinder onBind(Intent intent) {
return super.onBind(intent);
}
private void publishState(String status, boolean active) {
appStatus = status == null ? (active ? "CONNECTED" : "DISCONNECTED") : status;
appActive = active;
Intent state = new Intent(ACTION_STATE);
state.setPackage(getPackageName());
state.putExtra(EXTRA_ACTIVE, active);
state.putExtra(EXTRA_STATUS, appStatus);
try { sendBroadcast(state); } catch (Throwable ignored) {}
}
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= 26) {
NotificationManager nm = getSystemService(NotificationManager.class);
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
"DragonTCP VPN",
NotificationManager.IMPORTANCE_LOW
);
nm.createNotificationChannel(channel);
}
}
private Notification buildNotification(String status) {
Intent open = new Intent(this, MainActivity.class);
PendingIntent contentIntent = PendingIntent.getActivity(
this, 0, open,
PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)
);
Intent stopIntent = new Intent(this, DragonService.class).setAction(ACTION_STOP);
PendingIntent stopPending = PendingIntent.getService(
this, 1, stopIntent,
PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)
);
Notification.Builder b = Build.VERSION.SDK_INT >= 26
? new Notification.Builder(this, CHANNEL_ID)
: new Notification.Builder(this);
return b.setContentTitle("DragonTCP Lite")
.setContentText(status)
.setSmallIcon(android.R.drawable.stat_sys_upload_done)
.setContentIntent(contentIntent)
.setOngoing(true)
.addAction(new Notification.Action.Builder(
android.R.drawable.ic_menu_close_clear_cancel,
"STOP",
stopPending
).build())
.build();
}
private void updateNotification(String status) {
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
nm.notify(NOTIFICATION_ID, buildNotification(status));
}
}
@@ -1,180 +0,0 @@
package com.dragontcp.client;
import android.app.Activity;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.GradientDrawable;
import android.os.Build;
import android.os.Bundle;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
public class LogActivity extends Activity {
private static final int BG = Color.rgb(11, 15, 20);
private static final int CARD = Color.rgb(22, 28, 36);
private static final int BORDER = Color.rgb(43, 53, 66);
private static final int TEXT = Color.rgb(244, 247, 251);
private static final int MUTED = Color.rgb(139, 151, 168);
private static final int LOG_BG = Color.rgb(7, 10, 14);
private static final int LOG_TEXT = Color.rgb(179, 194, 214);
private TextView logText;
private ScrollView logScroll;
private final AppLog.Listener logListener = line -> runOnUiThread(() -> appendLogLine(line));
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
forceDarkSystemUi();
buildUi();
}
@Override
protected void onStart() {
super.onStart();
logText.setText(AppLog.history());
AppLog.addListener(logListener);
logScroll.post(this::scrollLogToBottom);
}
@Override
protected void onStop() {
AppLog.removeListener(logListener);
super.onStop();
}
private void forceDarkSystemUi() {
Window w = getWindow();
w.setStatusBarColor(BG);
w.setNavigationBarColor(Color.rgb(7, 10, 14));
if (Build.VERSION.SDK_INT >= 26) w.getDecorView().setSystemUiVisibility(0);
}
private void buildUi() {
LinearLayout root = new LinearLayout(this);
root.setOrientation(LinearLayout.VERTICAL);
root.setBackgroundColor(BG);
root.setPadding(dp(16), dp(14), dp(16), dp(12));
LinearLayout header = new LinearLayout(this);
header.setOrientation(LinearLayout.HORIZONTAL);
header.setGravity(Gravity.CENTER_VERTICAL);
header.setPadding(dp(2), dp(2), dp(2), dp(12));
LinearLayout heading = new LinearLayout(this);
heading.setOrientation(LinearLayout.VERTICAL);
TextView title = text("Logs", 25, TEXT, true);
TextView subtitle = text("DragonTCP Lite", 12, MUTED, false);
subtitle.setPadding(0, dp(2), 0, 0);
heading.addView(title);
heading.addView(subtitle);
header.addView(heading, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
Button back = smallButton("BACK");
Button clear = smallButton("CLEAR");
LinearLayout.LayoutParams bp = new LinearLayout.LayoutParams(dp(76), dp(42));
bp.setMarginEnd(dp(8));
header.addView(back, bp);
header.addView(clear, new LinearLayout.LayoutParams(dp(82), dp(42)));
root.addView(header);
LinearLayout panel = new LinearLayout(this);
panel.setOrientation(LinearLayout.VERTICAL);
panel.setPadding(dp(10), dp(10), dp(10), dp(10));
panel.setBackground(roundRect(CARD, 16, BORDER, 1));
logScroll = new ScrollView(this);
logScroll.setFillViewport(true);
logScroll.setFocusable(false);
logScroll.setOverScrollMode(View.OVER_SCROLL_IF_CONTENT_SCROLLS);
logScroll.setBackground(roundRect(LOG_BG, 11, Color.rgb(30, 38, 49), 1));
logText = new TextView(this);
logText.setTextSize(12f);
logText.setTextColor(LOG_TEXT);
logText.setTypeface(Typeface.MONOSPACE);
logText.setTextIsSelectable(true);
logText.setPadding(dp(11), dp(10), dp(11), dp(10));
logText.setLineSpacing(0, 1.10f);
logScroll.addView(logText, new ScrollView.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
));
panel.addView(logScroll, new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
0,
1f
));
root.addView(panel, new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
0,
1f
));
back.setOnClickListener(v -> finish());
clear.setOnClickListener(v -> {
AppLog.clear();
logText.setText("");
logScroll.scrollTo(0, 0);
});
setContentView(root);
}
private void appendLogLine(String line) {
View child = logScroll.getChildAt(0);
int contentHeight = child == null ? 0 : child.getHeight();
int maxScroll = Math.max(0, contentHeight - logScroll.getHeight());
boolean follow = maxScroll - logScroll.getScrollY() < dp(48);
logText.append(line + "\n");
if (follow) logScroll.post(this::scrollLogToBottom);
}
private void scrollLogToBottom() {
View child = logScroll.getChildAt(0);
if (child == null) return;
int y = Math.max(0, child.getHeight() - logScroll.getHeight());
logScroll.scrollTo(0, y);
}
private TextView text(String value, float size, int color, boolean bold) {
TextView t = new TextView(this);
t.setText(value);
t.setTextSize(size);
t.setTextColor(color);
if (bold) t.setTypeface(Typeface.DEFAULT, Typeface.BOLD);
return t;
}
private Button smallButton(String value) {
Button b = new Button(this);
b.setText(value);
b.setAllCaps(false);
b.setTextSize(11);
b.setTypeface(Typeface.DEFAULT, Typeface.BOLD);
b.setTextColor(TEXT);
b.setBackground(roundRect(Color.rgb(32, 40, 51), 10, BORDER, 1));
return b;
}
private GradientDrawable roundRect(int color, int radiusDp, int strokeColor, int strokeDp) {
GradientDrawable d = new GradientDrawable();
d.setColor(color);
d.setCornerRadius(dp(radiusDp));
if (strokeDp > 0) d.setStroke(dp(strokeDp), strokeColor);
return d;
}
private int dp(int value) {
return Math.round(value * getResources().getDisplayMetrics().density);
}
}
@@ -1,432 +0,0 @@
package com.dragontcp.client;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.GradientDrawable;
import android.net.VpnService;
import android.os.Build;
import android.os.Bundle;
import android.text.InputType;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends Activity {
private static final int VPN_REQUEST = 100;
private static final String PREFS = "dragontcp";
private static final int BG = Color.rgb(11, 15, 20);
private static final int CARD = Color.rgb(22, 28, 36);
private static final int FIELD = Color.rgb(14, 19, 26);
private static final int BORDER = Color.rgb(43, 53, 66);
private static final int TEXT = Color.rgb(244, 247, 251);
private static final int MUTED = Color.rgb(139, 151, 168);
private static final int ACCENT = Color.rgb(92, 149, 255);
private static final int STOP = Color.rgb(218, 83, 83);
private static final int DISABLED = Color.rgb(48, 56, 68);
private static final int OK = Color.rgb(82, 201, 143);
private static final int WARN = Color.rgb(240, 177, 83);
private EditText server;
private EditText port;
private EditText token;
private EditText chunkMax;
private EditText chunkMin;
private EditText reconnect;
private EditText timeout;
private Button connectButton;
private Button stopButton;
private Button logsButton;
private TextView statusChip;
private boolean receiverRegistered;
private boolean connectPending;
private final BroadcastReceiver stateReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (!DragonService.ACTION_STATE.equals(intent.getAction())) return;
boolean active = intent.getBooleanExtra(DragonService.EXTRA_ACTIVE, false);
String status = intent.getStringExtra(DragonService.EXTRA_STATUS);
connectPending = false;
updateConnectionUi(active, status == null ? (active ? "CONNECTED" : "DISCONNECTED") : status);
}
};
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
forceDarkSystemUi();
buildUi();
loadSettings();
}
@Override
protected void onStart() {
super.onStart();
if (!receiverRegistered) {
registerReceiver(stateReceiver, new IntentFilter(DragonService.ACTION_STATE));
receiverRegistered = true;
}
updateConnectionUi(DragonService.isActive(), DragonService.currentStatus());
}
@Override
protected void onStop() {
if (receiverRegistered) {
try { unregisterReceiver(stateReceiver); } catch (Throwable ignored) {}
receiverRegistered = false;
}
super.onStop();
}
private void forceDarkSystemUi() {
Window w = getWindow();
w.setStatusBarColor(BG);
w.setNavigationBarColor(Color.rgb(7, 10, 14));
if (Build.VERSION.SDK_INT >= 26) w.getDecorView().setSystemUiVisibility(0);
}
private void buildUi() {
LinearLayout root = new LinearLayout(this);
root.setOrientation(LinearLayout.VERTICAL);
root.setBackgroundColor(BG);
root.setPadding(dp(16), dp(14), dp(16), dp(12));
LinearLayout header = new LinearLayout(this);
header.setOrientation(LinearLayout.HORIZONTAL);
header.setGravity(Gravity.CENTER_VERTICAL);
header.setPadding(dp(2), dp(2), dp(2), dp(12));
LinearLayout heading = new LinearLayout(this);
heading.setOrientation(LinearLayout.VERTICAL);
TextView title = text("DragonTCP Lite", 25, TEXT, true);
TextView subtitle = text("Adaptive tunnel over TCP/53", 12, MUTED, false);
subtitle.setPadding(0, dp(2), 0, 0);
heading.addView(title);
heading.addView(subtitle);
header.addView(heading, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
statusChip = text("DISCONNECTED", 11, MUTED, true);
statusChip.setGravity(Gravity.CENTER);
statusChip.setPadding(dp(12), dp(7), dp(12), dp(7));
statusChip.setBackground(roundRect(CARD, 999, BORDER, 1));
header.addView(statusChip);
root.addView(header);
ScrollView scroll = new ScrollView(this);
scroll.setFillViewport(false);
scroll.setOverScrollMode(View.OVER_SCROLL_IF_CONTENT_SCROLLS);
scroll.setVerticalScrollBarEnabled(false);
LinearLayout settings = new LinearLayout(this);
settings.setOrientation(LinearLayout.VERTICAL);
settings.setPadding(0, 0, 0, dp(12));
scroll.addView(settings, new ScrollView.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
));
LinearLayout connectionCard = card("CONNECTION");
server = addField(connectionCard, "Server", "Server IP or hostname", "", false, false);
LinearLayout connectionRow = row();
port = addFieldToRow(connectionRow, "Port", "53", "53", true, false, 0.34f);
token = addFieldToRow(connectionRow, "Token", "Optional", "", false, true, 0.66f);
connectionCard.addView(connectionRow);
settings.addView(connectionCard, cardParams());
LinearLayout transportCard = card("TRANSPORT");
LinearLayout chunks = row();
chunkMax = addFieldToRow(chunks, "Max chunk", "1048576", "1048576", true, false, 0.58f);
chunkMin = addFieldToRow(chunks, "Min chunk", "32", "32", true, false, 0.42f);
transportCard.addView(chunks);
LinearLayout timing = row();
reconnect = addFieldToRow(timing, "Reconnect every", "1", "1", true, false, 0.58f);
timeout = addFieldToRow(timing, "Timeout (s)", "2", "2", true, false, 0.42f);
transportCard.addView(timing);
TextView fixed = text("1 poller • Start = Max chunk", 11, MUTED, false);
fixed.setPadding(dp(2), dp(6), dp(2), dp(2));
transportCard.addView(fixed);
settings.addView(transportCard, cardParams());
LinearLayout buttons = row();
buttons.setPadding(0, dp(4), 0, dp(8));
connectButton = actionButton("CONNECT");
stopButton = actionButton("STOP");
LinearLayout.LayoutParams left = new LinearLayout.LayoutParams(0, dp(52), 1f);
left.setMarginEnd(dp(6));
LinearLayout.LayoutParams right = new LinearLayout.LayoutParams(0, dp(52), 1f);
right.setMarginStart(dp(6));
buttons.addView(connectButton, left);
buttons.addView(stopButton, right);
settings.addView(buttons);
logsButton = actionButton("OPEN LOGS");
logsButton.setTextColor(TEXT);
logsButton.setBackground(roundRect(Color.rgb(32, 40, 51), 14, BORDER, 1));
LinearLayout.LayoutParams logsParams = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, dp(50));
logsParams.setMargins(0, 0, 0, dp(10));
settings.addView(logsButton, logsParams);
TextView note = text(
"IPv4 uses the Android VPN adapter. IPv6 is captured and blocked to prevent bypass. DNS is tunneled to 1.1.1.1 over DragonTCP.",
11, MUTED, false
);
note.setLineSpacing(0, 1.08f);
note.setPadding(dp(4), dp(2), dp(4), dp(6));
settings.addView(note);
root.addView(scroll, new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
0,
1f
));
connectButton.setOnClickListener(v -> requestConnect());
stopButton.setOnClickListener(v -> requestStop());
logsButton.setOnClickListener(v -> startActivity(new Intent(this, LogActivity.class)));
setContentView(root);
updateConnectionUi(false, "DISCONNECTED");
}
private LinearLayout card(String title) {
LinearLayout card = new LinearLayout(this);
card.setOrientation(LinearLayout.VERTICAL);
card.setPadding(dp(13), dp(11), dp(13), dp(12));
card.setBackground(roundRect(CARD, 16, BORDER, 1));
TextView label = text(title, 11, MUTED, true);
label.setLetterSpacing(0.08f);
label.setPadding(dp(2), 0, 0, dp(7));
card.addView(label);
return card;
}
private LinearLayout.LayoutParams cardParams() {
LinearLayout.LayoutParams p = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
);
p.setMargins(0, 0, 0, dp(10));
return p;
}
private EditText addField(LinearLayout parent, String label, String hint, String value, boolean numeric, boolean password) {
LinearLayout block = fieldBlock(label);
EditText edit = createEdit(hint, value, numeric, password);
block.addView(edit, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(46)));
parent.addView(block, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
return edit;
}
private EditText addFieldToRow(LinearLayout parent, String label, String hint, String value, boolean numeric, boolean password, float weight) {
LinearLayout block = fieldBlock(label);
EditText edit = createEdit(hint, value, numeric, password);
block.addView(edit, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(46)));
LinearLayout.LayoutParams p = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, weight);
if (parent.getChildCount() > 0) p.setMarginStart(dp(8));
parent.addView(block, p);
return edit;
}
private LinearLayout fieldBlock(String label) {
LinearLayout block = new LinearLayout(this);
block.setOrientation(LinearLayout.VERTICAL);
block.setPadding(0, 0, 0, dp(8));
TextView name = text(label, 11, MUTED, false);
name.setPadding(dp(2), 0, 0, dp(4));
block.addView(name);
return block;
}
private EditText createEdit(String hint, String value, boolean numeric, boolean password) {
EditText edit = new EditText(this);
edit.setSingleLine(true);
edit.setText(value);
edit.setHint(hint);
edit.setTextColor(TEXT);
edit.setHintTextColor(Color.rgb(91, 103, 120));
edit.setTextSize(15);
edit.setPadding(dp(12), 0, dp(12), 0);
edit.setBackground(roundRect(FIELD, 11, BORDER, 1));
edit.setSelectAllOnFocus(false);
if (numeric) edit.setInputType(InputType.TYPE_CLASS_NUMBER);
if (password) edit.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
return edit;
}
private LinearLayout row() {
LinearLayout row = new LinearLayout(this);
row.setOrientation(LinearLayout.HORIZONTAL);
return row;
}
private TextView text(String value, float size, int color, boolean bold) {
TextView t = new TextView(this);
t.setText(value);
t.setTextSize(size);
t.setTextColor(color);
if (bold) t.setTypeface(Typeface.DEFAULT, Typeface.BOLD);
return t;
}
private Button actionButton(String value) {
Button b = new Button(this);
b.setText(value);
b.setTextSize(14);
b.setTypeface(Typeface.DEFAULT, Typeface.BOLD);
b.setTextColor(Color.WHITE);
b.setAllCaps(false);
return b;
}
private GradientDrawable roundRect(int color, int radiusDp, int strokeColor, int strokeDp) {
GradientDrawable d = new GradientDrawable();
d.setColor(color);
d.setCornerRadius(dp(radiusDp));
if (strokeDp > 0) d.setStroke(dp(strokeDp), strokeColor);
return d;
}
private void requestConnect() {
if (connectPending || DragonService.isActive()) return;
try {
validateAndSave();
} catch (Exception e) {
String msg = e.getMessage() == null ? "Invalid configuration" : e.getMessage();
AppLog.append("CONFIG: " + msg);
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
return;
}
connectPending = true;
updateConnectionUi(true, "CONNECTING");
Intent prepare = VpnService.prepare(this);
if (prepare != null) startActivityForResult(prepare, VPN_REQUEST);
else startDragonService();
}
private void requestStop() {
if (!DragonService.isActive() && !connectPending) return;
connectPending = false;
updateConnectionUi(true, "STOPPING");
connectButton.setEnabled(false);
stopButton.setEnabled(false);
Intent i = new Intent(this, DragonService.class).setAction(DragonService.ACTION_STOP);
startService(i);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == VPN_REQUEST) {
if (resultCode == RESULT_OK) {
startDragonService();
} else {
connectPending = false;
updateConnectionUi(false, "DISCONNECTED");
AppLog.append("VPN permission was not granted");
}
}
}
private void startDragonService() {
SharedPreferences p = getSharedPreferences(PREFS, MODE_PRIVATE);
Intent i = new Intent(this, DragonService.class).setAction(DragonService.ACTION_CONNECT);
i.putExtra(DragonService.EXTRA_SERVER, p.getString("server", ""));
i.putExtra(DragonService.EXTRA_PORT, p.getInt("port", 53));
i.putExtra(DragonService.EXTRA_TOKEN, p.getString("token", ""));
i.putExtra(DragonService.EXTRA_CHUNK_MAX, p.getInt("max", 1048576));
i.putExtra(DragonService.EXTRA_CHUNK_MIN, p.getInt("min", 32));
i.putExtra(DragonService.EXTRA_RECONNECT, p.getInt("reconnect", 1));
i.putExtra(DragonService.EXTRA_TIMEOUT, p.getInt("timeout", 2));
if (Build.VERSION.SDK_INT >= 26) startForegroundService(i); else startService(i);
}
private void validateAndSave() {
String h = server.getText().toString().trim();
if (h.isEmpty()) throw new IllegalArgumentException("Server is required");
int p = parse(port, 1, 65535, "Port");
int max = parse(chunkMax, 32, 1048576, "Max chunk");
int min = parse(chunkMin, 32, max, "Min chunk");
int rec = parse(reconnect, 1, 1000000, "Reconnect every");
int tout = parse(timeout, 1, 120, "Timeout");
getSharedPreferences(PREFS, MODE_PRIVATE).edit()
.putString("server", h)
.putInt("port", p)
.putString("token", token.getText().toString())
.putInt("max", max)
.putInt("min", min)
.putInt("reconnect", rec)
.putInt("timeout", tout)
.apply();
}
private int parse(EditText field, int min, int max, String name) {
int v;
try { v = Integer.parseInt(field.getText().toString().trim()); }
catch (Exception e) { throw new IllegalArgumentException(name + " is invalid"); }
if (v < min || v > max) throw new IllegalArgumentException(name + " must be " + min + "-" + max);
return v;
}
private void loadSettings() {
SharedPreferences p = getSharedPreferences(PREFS, MODE_PRIVATE);
server.setText(p.getString("server", ""));
port.setText(Integer.toString(p.getInt("port", 53)));
token.setText(p.getString("token", ""));
chunkMax.setText(Integer.toString(p.getInt("max", 1048576)));
chunkMin.setText(Integer.toString(p.getInt("min", 32)));
reconnect.setText(Integer.toString(p.getInt("reconnect", 1)));
timeout.setText(Integer.toString(p.getInt("timeout", 2)));
}
private void updateConnectionUi(boolean active, String rawStatus) {
String status = rawStatus == null ? (active ? "CONNECTED" : "DISCONNECTED") : rawStatus.toUpperCase();
boolean stopping = status.contains("STOPPING");
boolean connected = status.contains("CONNECTED") && !status.contains("DISCONNECTED");
boolean starting = active && !connected && !stopping;
statusChip.setText(connected ? "CONNECTED" : starting ? "CONNECTING" : stopping ? "STOPPING" : "DISCONNECTED");
statusChip.setTextColor(connected ? OK : starting || stopping ? WARN : MUTED);
statusChip.setBackground(roundRect(
connected ? Color.rgb(20, 54, 42) : starting || stopping ? Color.rgb(58, 43, 22) : CARD,
999,
connected ? Color.rgb(46, 105, 80) : starting || stopping ? Color.rgb(112, 78, 35) : BORDER,
1
));
boolean canConnect = !active && !connectPending && !stopping;
boolean canStop = active && !stopping;
connectButton.setEnabled(canConnect);
stopButton.setEnabled(canStop);
connectButton.setTextColor(canConnect ? Color.WHITE : Color.rgb(132, 142, 155));
connectButton.setBackground(roundRect(canConnect ? ACCENT : DISABLED, 14, canConnect ? ACCENT : BORDER, 1));
stopButton.setTextColor(canStop ? Color.WHITE : Color.rgb(112, 121, 133));
stopButton.setBackground(roundRect(canStop ? STOP : Color.rgb(32, 38, 47), 14, canStop ? STOP : BORDER, 1));
}
private int dp(int value) {
return Math.round(value * getResources().getDisplayMetrics().density);
}
}