package com.dragontcp.client; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.Typeface; 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.widget.Button; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.ScrollView; import android.widget.TableLayout; import android.widget.TableRow; import android.widget.TextView; public class MainActivity extends Activity { private static final int VPN_REQUEST = 100; private static final String PREFS = "dragontcp"; private EditText server; private EditText port; private EditText token; private EditText chunkMax; private EditText chunkMin; private EditText reconnect; private EditText timeout; private TextView logText; private ScrollView logScroll; private final AppLog.Listener logListener = line -> runOnUiThread(() -> appendLogLine(line)); @Override public void onCreate(Bundle state) { super.onCreate(state); buildUi(); loadSettings(); } @Override protected void onStart() { super.onStart(); logText.setText(AppLog.history()); AppLog.addListener(logListener); logScroll.post(() -> logScroll.fullScroll(View.FOCUS_DOWN)); } @Override protected void onStop() { AppLog.removeListener(logListener); super.onStop(); } private void buildUi() { int pad = dp(14); ScrollView outer = new ScrollView(this); LinearLayout root = new LinearLayout(this); root.setOrientation(LinearLayout.VERTICAL); root.setPadding(pad, pad, pad, pad); outer.addView(root, new ScrollView.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT )); TextView title = new TextView(this); title.setText("DragonTCP Lite VPN"); title.setTextSize(24); title.setTypeface(Typeface.DEFAULT, Typeface.BOLD); root.addView(title); TextView subtitle = new TextView(this); subtitle.setText("Android VPN → local HTTP CONNECT proxy → adaptive XOR over TCP/53"); subtitle.setTextSize(13); subtitle.setPadding(0, dp(4), 0, dp(12)); root.addView(subtitle); TableLayout table = new TableLayout(this); table.setStretchAllColumns(false); table.setColumnStretchable(1, true); root.addView(table, new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT )); server = addField(table, "Server", "", false, false); port = addField(table, "Port", "53", true, false); token = addField(table, "Token", "", false, true); chunkMax = addField(table, "Max chunk", "1048576", true, false); chunkMin = addField(table, "Min chunk", "32", true, false); reconnect = addField(table, "Reconnect every", "1", true, false); timeout = addField(table, "Timeout (s)", "2", true, false); TextView fixed = new TextView(this); fixed.setText("Pollers: 1 (fixed) • Start chunk = Max chunk • XOR 0xAD always on"); fixed.setTextSize(12); fixed.setPadding(0, dp(8), 0, dp(8)); root.addView(fixed); LinearLayout buttons = new LinearLayout(this); buttons.setOrientation(LinearLayout.HORIZONTAL); buttons.setGravity(Gravity.CENTER); Button connect = new Button(this); connect.setText("CONNECT"); Button stop = new Button(this); stop.setText("STOP"); buttons.addView(connect, new LinearLayout.LayoutParams(0, dp(52), 1f)); buttons.addView(stop, new LinearLayout.LayoutParams(0, dp(52), 1f)); root.addView(buttons); connect.setOnClickListener(v -> requestConnect()); stop.setOnClickListener(v -> { Intent i = new Intent(this, DragonService.class).setAction(DragonService.ACTION_STOP); startService(i); }); LinearLayout logHeader = new LinearLayout(this); logHeader.setOrientation(LinearLayout.HORIZONTAL); logHeader.setGravity(Gravity.CENTER_VERTICAL); TextView logLabel = new TextView(this); logLabel.setText("Live log"); logLabel.setTextSize(16); logLabel.setTypeface(Typeface.DEFAULT, Typeface.BOLD); Button clear = new Button(this); clear.setText("CLEAR"); logHeader.addView(logLabel, new LinearLayout.LayoutParams(0, dp(48), 1f)); logHeader.addView(clear, new LinearLayout.LayoutParams(dp(100), dp(48))); root.addView(logHeader); logScroll = new ScrollView(this); logText = new TextView(this); logText.setTextSize(12); logText.setTypeface(Typeface.MONOSPACE); logText.setTextIsSelectable(true); logText.setPadding(dp(8), dp(8), dp(8), dp(8)); logScroll.addView(logText, new ScrollView.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT )); root.addView(logScroll, new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, dp(280) )); clear.setOnClickListener(v -> { AppLog.clear(); logText.setText(""); }); TextView note = new TextView(this); note.setText("IPv4 is tunneled. IPv6 is captured and blocked so it cannot bypass the proxy. DNS is sent to 1.1.1.1 through DragonTCP using DNS-over-TCP."); note.setTextSize(11); note.setPadding(0, dp(8), 0, dp(12)); root.addView(note); setContentView(outer); } private EditText addField(TableLayout table, String label, String defaultValue, boolean numeric, boolean password) { TableRow row = new TableRow(this); row.setPadding(0, dp(2), 0, dp(2)); TextView name = new TextView(this); name.setText(label); name.setGravity(Gravity.CENTER_VERTICAL); name.setPadding(0, 0, dp(10), 0); EditText value = new EditText(this); value.setSingleLine(true); value.setText(defaultValue); if (numeric) value.setInputType(InputType.TYPE_CLASS_NUMBER); if (password) value.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); row.addView(name, new TableRow.LayoutParams(dp(125), dp(50))); row.addView(value, new TableRow.LayoutParams(0, dp(50), 1f)); table.addView(row); return value; } private void requestConnect() { try { validateAndSave(); } catch (Exception e) { AppLog.append("CONFIG: " + e.getMessage()); return; } Intent prepare = VpnService.prepare(this); if (prepare != null) { startActivityForResult(prepare, VPN_REQUEST); } else { startDragonService(); } } @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 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 appendLogLine(String line) { // Only auto-scroll when the user was already near the bottom. View child = logScroll.getChildAt(0); int gap = child == null ? 0 : child.getBottom() - (logScroll.getScrollY() + logScroll.getHeight()); boolean follow = gap < dp(48); logText.append(line + "\n"); if (follow) logScroll.post(() -> logScroll.fullScroll(View.FOCUS_DOWN)); } private int dp(int value) { return Math.round(value * getResources().getDisplayMetrics().density); } }