v11
This commit is contained in:
@@ -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.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
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;
|
||||
@@ -35,11 +35,8 @@ public class MainActivity extends Activity {
|
||||
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 ACCENT_DARK = Color.rgb(42, 67, 104);
|
||||
private static final int STOP = Color.rgb(218, 83, 83);
|
||||
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 WARN = Color.rgb(240, 177, 83);
|
||||
|
||||
@@ -53,15 +50,12 @@ public class MainActivity extends Activity {
|
||||
|
||||
private Button connectButton;
|
||||
private Button stopButton;
|
||||
private Button logsButton;
|
||||
private TextView statusChip;
|
||||
private TextView logText;
|
||||
private ScrollView logScroll;
|
||||
|
||||
private boolean receiverRegistered;
|
||||
private boolean connectPending;
|
||||
|
||||
private final AppLog.Listener logListener = line -> runOnUiThread(() -> appendLogLine(line));
|
||||
|
||||
private final BroadcastReceiver stateReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
@@ -84,13 +78,6 @@ public class MainActivity extends Activity {
|
||||
@Override
|
||||
protected void 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) {
|
||||
registerReceiver(stateReceiver, new IntentFilter(DragonService.ACTION_STATE));
|
||||
receiverRegistered = true;
|
||||
@@ -100,7 +87,6 @@ public class MainActivity extends Activity {
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
AppLog.removeListener(logListener);
|
||||
if (receiverRegistered) {
|
||||
try { unregisterReceiver(stateReceiver); } catch (Throwable ignored) {}
|
||||
receiverRegistered = false;
|
||||
@@ -112,9 +98,7 @@ public class MainActivity extends Activity {
|
||||
Window w = getWindow();
|
||||
w.setStatusBarColor(BG);
|
||||
w.setNavigationBarColor(Color.rgb(7, 10, 14));
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
w.getDecorView().setSystemUiVisibility(0);
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 26) w.getDecorView().setSystemUiVisibility(0);
|
||||
}
|
||||
|
||||
private void buildUi() {
|
||||
@@ -122,10 +106,7 @@ public class MainActivity extends Activity {
|
||||
root.setOrientation(LinearLayout.VERTICAL);
|
||||
root.setBackgroundColor(BG);
|
||||
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);
|
||||
header.setOrientation(LinearLayout.HORIZONTAL);
|
||||
header.setGravity(Gravity.CENTER_VERTICAL);
|
||||
@@ -147,42 +128,18 @@ public class MainActivity extends Activity {
|
||||
header.addView(statusChip);
|
||||
root.addView(header);
|
||||
|
||||
// The remaining screen is an explicit vertical split. The settings pane and
|
||||
// Live Log pane are siblings; neither is drawn over the other.
|
||||
LinearLayout split = new LinearLayout(this);
|
||||
split.setOrientation(LinearLayout.VERTICAL);
|
||||
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);
|
||||
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(8));
|
||||
settingsScroll.addView(settings, new ScrollView.LayoutParams(
|
||||
settings.setPadding(0, 0, 0, dp(12));
|
||||
scroll.addView(settings, new ScrollView.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
));
|
||||
settingsPane.addView(settingsScroll, new FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
));
|
||||
|
||||
LinearLayout connectionCard = card("CONNECTION");
|
||||
server = addField(connectionCard, "Server", "Server IP or hostname", "", false, false);
|
||||
@@ -220,6 +177,14 @@ public class MainActivity extends Activity {
|
||||
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
|
||||
@@ -228,78 +193,15 @@ public class MainActivity extends Activity {
|
||||
note.setPadding(dp(4), dp(2), dp(4), dp(6));
|
||||
settings.addView(note);
|
||||
|
||||
// Give settings the upper portion of the available body. This FrameLayout
|
||||
// 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(
|
||||
root.addView(scroll, new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
0,
|
||||
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());
|
||||
stopButton.setOnClickListener(v -> requestStop());
|
||||
logsButton.setOnClickListener(v -> startActivity(new Intent(this, LogActivity.class)));
|
||||
|
||||
setContentView(root);
|
||||
updateConnectionUi(false, "DISCONNECTED");
|
||||
@@ -395,13 +297,6 @@ public class MainActivity extends Activity {
|
||||
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) {
|
||||
GradientDrawable d = new GradientDrawable();
|
||||
d.setColor(color);
|
||||
@@ -415,20 +310,18 @@ public class MainActivity extends Activity {
|
||||
try {
|
||||
validateAndSave();
|
||||
} 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;
|
||||
}
|
||||
|
||||
// Grey it immediately, before Android's VPN permission dialog can be double-tapped.
|
||||
connectPending = true;
|
||||
updateConnectionUi(true, "CONNECTING");
|
||||
|
||||
Intent prepare = VpnService.prepare(this);
|
||||
if (prepare != null) {
|
||||
startActivityForResult(prepare, VPN_REQUEST);
|
||||
} else {
|
||||
startDragonService();
|
||||
}
|
||||
if (prepare != null) startActivityForResult(prepare, VPN_REQUEST);
|
||||
else startDragonService();
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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) {
|
||||
return Math.round(value * getResources().getDisplayMetrics().density);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user