270 lines
10 KiB
Java
270 lines
10 KiB
Java
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);
|
|
}
|
|
}
|