V13
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user