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