This commit is contained in:
2026-08-16 04:01:37 -03:00
parent bb8b966a75
commit c0a337be3f
4 changed files with 244 additions and 353 deletions
+32 -200
View File
@@ -1,222 +1,54 @@
# DragonTCP Lite VPN # DragonTCP LiteVPN v11 - Separate Logs Page
This version deliberately returns to the lightweight DragonTCP architecture. This release changes only the Android UI. The working DragonTCP Lite transport core and server binaries are unchanged from the previous Lite build.
DragonTCP itself is an HTTP/HTTPS CONNECT proxy tunnel; Android's `VpnService`
is only the local adapter that feeds normal app TCP traffic into that proxy.
## Architecture ## UI change
```text The main screen now contains only:
Android apps
|
| IPv4 TCP / DNS packets
v
Android VpnService TUN
|
| lightweight userspace TCP adapter
v
127.0.0.1:8080 HTTP CONNECT
|
v
DragonTCP Go client
|
| adaptive records + mandatory XOR 0xAD
| TCP/53
v
DragonTCP Lite server
|
v
Internet destination
```
There is **no Linux TUN**, no server NAT, no raw-IP DragonTCP protocol, and no - Server
packet batching on the DragonTCP server. The remote side is the same style of - Port
lightweight stream proxy that worked in the earlier Termux tests. - Optional token
- Max/min chunk
- Reconnect Every
- Timeout
- CONNECT
- STOP
- OPEN LOGS
- Connection status
## Defaults The Live Log is no longer present anywhere in the main layout.
Android: Press **OPEN LOGS** to open a dedicated full-screen Logs Activity. That page has its own scrolling area, BACK button, and CLEAR button. Log updates therefore cannot resize, overlay, or move the settings page.
```text ## Network behavior unchanged
Server port: 53/TCP
Token: optional / empty allowed
Local proxy: 127.0.0.1:8080
Pollers: 1 (fixed)
Reconnect Every: 1
Chunk Start: Max Chunk
Chunk Min: 32
Chunk Max: 1,048,576 bytes
Chunk Grow After: 16 successes
Chunk timeout: 2 seconds
XOR: 0xAD, mandatory
DNS: 1.1.1.1 through DNS-over-TCP through DragonTCP
```
The Android log is intentionally quiet. It shows service state, actual errors, - TCP/53 transport
and DragonTCP adaptive changes such as: - mandatory existing DragonTCP payload transform
- adaptive chunks
- 1 fixed poller
- reconnect-every default 1
- max chunk 1 MiB
- optional empty token
- Android VPN/TUN frontend from the Lite build
```text The embedded Android DragonTCP core and both supplied server binaries are byte-for-byte identical to the previous Lite SplitUI build.
adaptive upload chunk: 65536 -> 32768 after transport failure
adaptive download chunk: 32 -> 64 after stable success
```
It does not redraw the screen or print periodic traffic statistics. ## Build
## Android traffic behavior The full source tree includes the existing build scripts. Set the Android SDK and Kotlin compiler paths and run:
- IPv4 TCP: forwarded through DragonTCP.
- DNS UDP/53: converted to DNS-over-TCP and sent to `1.1.1.1` through the local
DragonTCP HTTP CONNECT proxy.
- IPv6: captured by the VPN and dropped so it cannot bypass DragonTCP.
- Other UDP: not forwarded in this HTTP CONNECT build.
- ICMP/ping: not forwarded.
This trade-off keeps DragonTCP itself lightweight and stream-oriented.
## Start the server
Port 53 is privileged on Linux, so run as root:
```bash
sudo ./dragontcp-lite-server-linux-amd64 --chunk-max 1048576
```
No token is required by default. To require one:
```bash
sudo ./dragontcp-lite-server-linux-amd64 --token 'SECRET' --chunk-max 1048576
```
The Android Token field must contain the same value.
If you do not want to run the binary as root:
```bash
sudo setcap cap_net_bind_service=+ep ./dragontcp-lite-server-linux-amd64
./dragontcp-lite-server-linux-amd64
```
If TCP/53 is already occupied by a DNS resolver, free that port first.
## Android usage
1. Install `DragonTCP-LiteVPN-arm64.apk`.
2. Enter the server address.
3. Leave Port at `53`.
4. Leave Token empty if the server was started without `--token`.
5. Keep `Reconnect every = 1` for restrictive networks.
6. Press **CONNECT** and approve Android's VPN dialog.
7. Press **STOP** to terminate both the TUN adapter and DragonTCP core.
The app excludes its own UID from the VPN, so the DragonTCP TCP/53 transport
uses the physical/mobile network and does not loop into its own TUN interface.
## Build everything from source
Requirements:
- Go 1.22+
- JDK 17+
- Android SDK platform + build-tools (35 works)
- Android 10 / API 29 or newer on the phone
- Kotlin compiler 1.9.x distribution containing
`kotlinx-coroutines-core-jvm.jar`
- `zip`
Example environment:
```bash
export ANDROID_SDK_ROOT="$HOME/Android/Sdk"
export KOTLIN_HOME="$HOME/.sdkman/candidates/kotlin/current"
```
Then:
```bash ```bash
./build_all.sh ./build_all.sh
``` ```
Outputs: Or build only the APK after the core already exists:
```text
bin/dragontcp-lite-server-linux-amd64
bin/dragontcp-lite-server-linux-arm64
android/lib/arm64-v8a/libdragontcp_client.so
android/build/DragonTCP-LiteVPN-arm64.apk
```
`libdragontcp_client.so` is intentionally the Android ARM64 Go executable stored
in the APK native-library directory. `DragonService` launches it with
`ProcessBuilder`; it is not JNI.
## Build only the Go core/server
```bash
./build_core.sh
```
## Build only the APK
After the core has been built:
```bash ```bash
cd android cd android
ANDROID_SDK_ROOT=/path/to/android-sdk \
KOTLIN_HOME=/path/to/kotlin \
./build_apk.sh ./build_apk.sh
``` ```
The script creates a local debug signing key if one does not already exist. For an APK that updates the supplied release, sign with the same signing key/certificate used by the previous DragonTCP APK.
For production distribution, supply your own keystore/signing process.
## Source layout
```text
core/ DragonTCP Go client/server
android/src/com/dragontcp/client/ Android UI + VpnService
android/src/tech/xvanturing/... Lightweight TUN-to-proxy stack
android/lib/arm64-v8a/ Embedded DragonTCP Android core
licenses/ Third-party licenses
```
## Third-party stack
The userspace Android TUN/TCP adapter is adapted from the Apache-2.0-licensed
FreeProxy project. See `THIRD_PARTY_NOTICES.md` and
`licenses/FreeProxy-APACHE-2.0.txt`.
## Validation performed for this package
- Go unit/build checks: PASS.
- Empty-token client/server protocol: PASS.
- Fixed 1 poller + reconnect every 1: PASS.
- 8 MiB HTTP transfer through local DragonTCP proxy: SHA-256 exact.
- Kotlin TUN adapter compilation: PASS.
- Android Java service/UI compilation: PASS.
- ARM64 Android DragonTCP core build: PASS.
- APK resource/DEX/native packaging: PASS.
- APK signature verification (v3): PASS.
- Supplied APK uses the same signing certificate as the previous Lite/v7 APK and has versionCode 10, so it can be installed as an in-place update.
A physical Android device is still required to validate the final VpnService
path against a real mobile network.
## v10 split UI update
This release changes only the Android UI. The embedded DragonTCP transport
core and the Lite server are unchanged from the working Lite release.
UI changes:
- Live Log is a true sibling pane below the settings pane; it is never drawn
over the settings.
- The settings pane is explicitly clipped to its own region.
- The settings scrollbar is hidden, so no scrollbar can appear behind the log.
- Live Log keeps its own internal scroll area and receives appended lines
without rebuilding the Activity.
- A visible divider separates settings from Live Log.
- CONNECT remains disabled while connecting/connected and STOP remains state-aware.
- The visible XOR wording was removed from the Android UI. Transport behavior
itself is unchanged.
- The Activity explicitly uses `adjustResize` when the keyboard opens.
The APK is versionCode 10 / versionName `10.0-lite-split-ui` and is signed with
the same certificate as the previous Lite UI build.
+8 -2
View File
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.dragontcp.client" package="com.dragontcp.client"
android:versionCode="10" android:versionCode="11"
android:versionName="10.0-lite-split-ui"> android:versionName="11.0-lite-logs-page">
<uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29" /> <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
@@ -27,6 +27,12 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".LogActivity"
android:exported="false"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustNothing" />
<service <service
android:name=".DragonService" android:name=".DragonService"
android:exported="true" android:exported="true"
@@ -0,0 +1,180 @@
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);
}
}
@@ -19,10 +19,10 @@ import android.view.ViewGroup;
import android.view.Window; import android.view.Window;
import android.widget.Button; import android.widget.Button;
import android.widget.EditText; import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.ScrollView; import android.widget.ScrollView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends Activity { public class MainActivity extends Activity {
private static final int VPN_REQUEST = 100; private static final int VPN_REQUEST = 100;
@@ -35,11 +35,8 @@ public class MainActivity extends Activity {
private static final int TEXT = Color.rgb(244, 247, 251); private static final int TEXT = Color.rgb(244, 247, 251);
private static final int MUTED = Color.rgb(139, 151, 168); private static final int MUTED = Color.rgb(139, 151, 168);
private static final int ACCENT = Color.rgb(92, 149, 255); private static final int ACCENT = Color.rgb(92, 149, 255);
private static final int ACCENT_DARK = Color.rgb(42, 67, 104);
private static final int STOP = Color.rgb(218, 83, 83); private static final int STOP = Color.rgb(218, 83, 83);
private static final int DISABLED = Color.rgb(48, 56, 68); private static final int DISABLED = Color.rgb(48, 56, 68);
private static final int LOG_BG = Color.rgb(7, 10, 14);
private static final int LOG_TEXT = Color.rgb(179, 194, 214);
private static final int OK = Color.rgb(82, 201, 143); private static final int OK = Color.rgb(82, 201, 143);
private static final int WARN = Color.rgb(240, 177, 83); private static final int WARN = Color.rgb(240, 177, 83);
@@ -53,15 +50,12 @@ public class MainActivity extends Activity {
private Button connectButton; private Button connectButton;
private Button stopButton; private Button stopButton;
private Button logsButton;
private TextView statusChip; private TextView statusChip;
private TextView logText;
private ScrollView logScroll;
private boolean receiverRegistered; private boolean receiverRegistered;
private boolean connectPending; private boolean connectPending;
private final AppLog.Listener logListener = line -> runOnUiThread(() -> appendLogLine(line));
private final BroadcastReceiver stateReceiver = new BroadcastReceiver() { private final BroadcastReceiver stateReceiver = new BroadcastReceiver() {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
@@ -84,13 +78,6 @@ public class MainActivity extends Activity {
@Override @Override
protected void onStart() { protected void onStart() {
super.onStart(); super.onStart();
// Load log history only when the Activity itself enters the foreground.
// Live updates below append only to the log TextView and never rebuild the UI.
logText.setText(AppLog.history());
AppLog.addListener(logListener);
logScroll.post(this::scrollLogToBottom);
if (!receiverRegistered) { if (!receiverRegistered) {
registerReceiver(stateReceiver, new IntentFilter(DragonService.ACTION_STATE)); registerReceiver(stateReceiver, new IntentFilter(DragonService.ACTION_STATE));
receiverRegistered = true; receiverRegistered = true;
@@ -100,7 +87,6 @@ public class MainActivity extends Activity {
@Override @Override
protected void onStop() { protected void onStop() {
AppLog.removeListener(logListener);
if (receiverRegistered) { if (receiverRegistered) {
try { unregisterReceiver(stateReceiver); } catch (Throwable ignored) {} try { unregisterReceiver(stateReceiver); } catch (Throwable ignored) {}
receiverRegistered = false; receiverRegistered = false;
@@ -112,9 +98,7 @@ public class MainActivity extends Activity {
Window w = getWindow(); Window w = getWindow();
w.setStatusBarColor(BG); w.setStatusBarColor(BG);
w.setNavigationBarColor(Color.rgb(7, 10, 14)); w.setNavigationBarColor(Color.rgb(7, 10, 14));
if (Build.VERSION.SDK_INT >= 26) { if (Build.VERSION.SDK_INT >= 26) w.getDecorView().setSystemUiVisibility(0);
w.getDecorView().setSystemUiVisibility(0);
}
} }
private void buildUi() { private void buildUi() {
@@ -122,10 +106,7 @@ public class MainActivity extends Activity {
root.setOrientation(LinearLayout.VERTICAL); root.setOrientation(LinearLayout.VERTICAL);
root.setBackgroundColor(BG); root.setBackgroundColor(BG);
root.setPadding(dp(16), dp(14), dp(16), dp(12)); root.setPadding(dp(16), dp(14), dp(16), dp(12));
root.setClipChildren(true);
root.setClipToPadding(true);
// Header is outside both scrolling regions.
LinearLayout header = new LinearLayout(this); LinearLayout header = new LinearLayout(this);
header.setOrientation(LinearLayout.HORIZONTAL); header.setOrientation(LinearLayout.HORIZONTAL);
header.setGravity(Gravity.CENTER_VERTICAL); header.setGravity(Gravity.CENTER_VERTICAL);
@@ -147,42 +128,18 @@ public class MainActivity extends Activity {
header.addView(statusChip); header.addView(statusChip);
root.addView(header); root.addView(header);
// The remaining screen is an explicit vertical split. The settings pane and ScrollView scroll = new ScrollView(this);
// Live Log pane are siblings; neither is drawn over the other. scroll.setFillViewport(false);
LinearLayout split = new LinearLayout(this); scroll.setOverScrollMode(View.OVER_SCROLL_IF_CONTENT_SCROLLS);
split.setOrientation(LinearLayout.VERTICAL); scroll.setVerticalScrollBarEnabled(false);
split.setClipChildren(true);
split.setClipToPadding(true);
root.addView(split, new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
0,
1f
));
FrameLayout settingsPane = new FrameLayout(this);
settingsPane.setBackgroundColor(BG);
settingsPane.setClipChildren(true);
settingsPane.setClipToPadding(true);
ScrollView settingsScroll = new ScrollView(this);
settingsScroll.setFillViewport(false);
settingsScroll.setClipToPadding(true);
settingsScroll.setClipChildren(true);
settingsScroll.setOverScrollMode(View.OVER_SCROLL_IF_CONTENT_SCROLLS);
// Avoid a scrollbar visually continuing into the separate log pane.
settingsScroll.setVerticalScrollBarEnabled(false);
LinearLayout settings = new LinearLayout(this); LinearLayout settings = new LinearLayout(this);
settings.setOrientation(LinearLayout.VERTICAL); settings.setOrientation(LinearLayout.VERTICAL);
settings.setPadding(0, 0, 0, dp(8)); settings.setPadding(0, 0, 0, dp(12));
settingsScroll.addView(settings, new ScrollView.LayoutParams( scroll.addView(settings, new ScrollView.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT ViewGroup.LayoutParams.WRAP_CONTENT
)); ));
settingsPane.addView(settingsScroll, new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
));
LinearLayout connectionCard = card("CONNECTION"); LinearLayout connectionCard = card("CONNECTION");
server = addField(connectionCard, "Server", "Server IP or hostname", "", false, false); server = addField(connectionCard, "Server", "Server IP or hostname", "", false, false);
@@ -220,6 +177,14 @@ public class MainActivity extends Activity {
buttons.addView(stopButton, right); buttons.addView(stopButton, right);
settings.addView(buttons); 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( 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.", "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 11, MUTED, false
@@ -228,78 +193,15 @@ public class MainActivity extends Activity {
note.setPadding(dp(4), dp(2), dp(4), dp(6)); note.setPadding(dp(4), dp(2), dp(4), dp(6));
settings.addView(note); settings.addView(note);
// Give settings the upper portion of the available body. This FrameLayout root.addView(scroll, new LinearLayout.LayoutParams(
// clips its ScrollView, making it impossible for settings to draw behind logs.
split.addView(settingsPane, new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
0,
0.58f
));
View divider = new View(this);
divider.setBackgroundColor(BORDER);
LinearLayout.LayoutParams dividerParams = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
dp(1)
);
dividerParams.setMargins(dp(6), dp(7), dp(6), dp(7));
split.addView(divider, dividerParams);
// Live Log is a true sibling below settings, not an overlay.
LinearLayout logPanel = new LinearLayout(this);
logPanel.setOrientation(LinearLayout.VERTICAL);
logPanel.setClipChildren(true);
logPanel.setClipToPadding(true);
logPanel.setBackground(roundRect(CARD, 16, BORDER, 1));
logPanel.setPadding(dp(10), dp(8), dp(10), dp(10));
LinearLayout logHeader = row();
logHeader.setGravity(Gravity.CENTER_VERTICAL);
TextView logLabel = text("Live log", 15, TEXT, true);
Button clear = smallButton("CLEAR");
logHeader.addView(logLabel, new LinearLayout.LayoutParams(0, dp(40), 1f));
logHeader.addView(clear, new LinearLayout.LayoutParams(dp(82), dp(38)));
logPanel.addView(logHeader);
logScroll = new ScrollView(this);
logScroll.setFillViewport(true);
logScroll.setFocusable(false);
logScroll.setClipChildren(true);
logScroll.setClipToPadding(true);
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(11.5f);
logText.setTextColor(LOG_TEXT);
logText.setTypeface(Typeface.MONOSPACE);
logText.setTextIsSelectable(true);
logText.setPadding(dp(10), dp(9), dp(10), dp(9));
logText.setLineSpacing(0, 1.08f);
logScroll.addView(logText, new ScrollView.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
));
logPanel.addView(logScroll, new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
0, 0,
1f 1f
)); ));
split.addView(logPanel, new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
0,
0.42f
));
clear.setOnClickListener(v -> {
AppLog.clear();
logText.setText("");
logScroll.scrollTo(0, 0);
});
connectButton.setOnClickListener(v -> requestConnect()); connectButton.setOnClickListener(v -> requestConnect());
stopButton.setOnClickListener(v -> requestStop()); stopButton.setOnClickListener(v -> requestStop());
logsButton.setOnClickListener(v -> startActivity(new Intent(this, LogActivity.class)));
setContentView(root); setContentView(root);
updateConnectionUi(false, "DISCONNECTED"); updateConnectionUi(false, "DISCONNECTED");
@@ -395,13 +297,6 @@ public class MainActivity extends Activity {
return b; return b;
} }
private Button smallButton(String value) {
Button b = actionButton(value);
b.setTextSize(11);
b.setBackground(roundRect(Color.rgb(32, 40, 51), 10, BORDER, 1));
return b;
}
private GradientDrawable roundRect(int color, int radiusDp, int strokeColor, int strokeDp) { private GradientDrawable roundRect(int color, int radiusDp, int strokeColor, int strokeDp) {
GradientDrawable d = new GradientDrawable(); GradientDrawable d = new GradientDrawable();
d.setColor(color); d.setColor(color);
@@ -415,20 +310,18 @@ public class MainActivity extends Activity {
try { try {
validateAndSave(); validateAndSave();
} catch (Exception e) { } catch (Exception e) {
AppLog.append("CONFIG: " + e.getMessage()); String msg = e.getMessage() == null ? "Invalid configuration" : e.getMessage();
AppLog.append("CONFIG: " + msg);
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
return; return;
} }
// Grey it immediately, before Android's VPN permission dialog can be double-tapped.
connectPending = true; connectPending = true;
updateConnectionUi(true, "CONNECTING"); updateConnectionUi(true, "CONNECTING");
Intent prepare = VpnService.prepare(this); Intent prepare = VpnService.prepare(this);
if (prepare != null) { if (prepare != null) startActivityForResult(prepare, VPN_REQUEST);
startActivityForResult(prepare, VPN_REQUEST); else startDragonService();
} else {
startDragonService();
}
} }
private void requestStop() { private void requestStop() {
@@ -533,26 +426,6 @@ public class MainActivity extends Activity {
stopButton.setBackground(roundRect(canStop ? STOP : Color.rgb(32, 38, 47), 14, canStop ? STOP : BORDER, 1)); stopButton.setBackground(roundRect(canStop ? STOP : Color.rgb(32, 38, 47), 14, canStop ? STOP : BORDER, 1));
} }
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(36);
logText.append(line + "\n");
// Do not use fullScroll(FOCUS_DOWN): it requests focus and can make an outer
// ScrollView jump. scrollTo only moves this dedicated fixed-size log panel.
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 int dp(int value) { private int dp(int value) {
return Math.round(value * getResources().getDisplayMetrics().density); return Math.round(value * getResources().getDisplayMetrics().density);
} }