This commit is contained in:
2026-07-22 17:42:32 -03:00
commit 3d36f2421e
645 changed files with 163624 additions and 0 deletions
+40
View File
@@ -0,0 +1,40 @@
plugins {
id 'com.android.application'
}
android {
namespace 'com.dragonssh.xhttpdemo'
compileSdk 36
defaultConfig {
applicationId 'com.dragonssh.xhttpdemo'
minSdk 26
targetSdk 36
versionCode 8
versionName '1.0.7'
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
// tun2socks is a native executable that must be run from nativeLibraryDir,
// which is the only app-owned location Android lets you exec from (API 29+
// blocks exec out of the writable data dir under SELinux). Legacy packaging
// extracts libtun2socks.so to nativeLibraryDir at install so CustomNativeLoader
// can run it directly. Without this the loader falls back to files/libtun2socks
// and exec fails with "error=13, Permission denied".
packagingOptions {
jniLibs {
useLegacyPackaging = true
}
}
}
dependencies {
implementation project(':service')
implementation 'androidx.appcompat:appcompat:1.7.1'
implementation 'com.google.android.material:material:1.13.0'
implementation 'androidx.recyclerview:recyclerview:1.4.0'
}
+1
View File
@@ -0,0 +1 @@
# Public example: no custom obfuscation rules required.
+38
View File
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<application
android:name=".DemoApplication"
android:allowBackup="false"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.DragonSshXHttpDemo"
android:usesCleartextTraffic="true">
<activity
android:name=".LogsActivity"
android:exported="false" />
<activity
android:name=".VpnSettingsActivity"
android:exported="false"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".SettingsActivity"
android:exported="false"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".MainActivity"
android:exported="true"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,44 @@
package com.dragonssh.xhttpdemo;
final class ConfigUtils {
private ConfigUtils() { }
static String normalizePath(String path) {
if (path == null || path.trim().isEmpty()) return "";
String normalized = path.trim();
if (!normalized.startsWith("/")) normalized = "/" + normalized;
while (normalized.length() > 1 && normalized.endsWith("/")) {
normalized = normalized.substring(0, normalized.length() - 1);
}
return normalized;
}
static boolean isValidPort(String value) {
try {
int port = Integer.parseInt(value);
return port >= 1 && port <= 65535;
} catch (NumberFormatException e) {
return false;
}
}
static boolean isHostPort(String value) {
if (value == null || value.trim().isEmpty()) return false;
String clean = value.trim();
int separator = clean.lastIndexOf(':');
if (separator <= 0 || separator == clean.length() - 1) return false;
return isValidPort(clean.substring(separator + 1));
}
static String displayValue(String value) {
return value == null || value.trim().isEmpty() ? "" : value.trim();
}
static String formatHostPort(String host, String port) {
String cleanHost = displayValue(host);
if (cleanHost.contains(":") && !cleanHost.startsWith("[") && !"".equals(cleanHost)) {
cleanHost = "[" + cleanHost + "]";
}
return cleanHost + ":" + displayValue(port);
}
}
@@ -0,0 +1,43 @@
package com.dragonssh.xhttpdemo;
import android.app.Application;
import android.content.SharedPreferences;
import com.dragonssh.xhttpdemo.core.XHttpSshCore;
import com.dragonssh.xhttpdemo.core.config.Settings;
public final class DemoApplication extends Application {
private static final String BOOTSTRAP_PREFS = "demo_bootstrap";
private static final String DEFAULTS_CREATED = "defaults_created";
private volatile String savedPassword = "";
private Settings settings;
@Override
public void onCreate() {
super.onCreate();
SharedPreferences prefs = getSharedPreferences(BOOTSTRAP_PREFS, MODE_PRIVATE);
if (!prefs.getBoolean(DEFAULTS_CREATED, false)) {
Settings.setDefaultConfig(this);
prefs.edit().putBoolean(DEFAULTS_CREATED, true).apply();
}
settings = new Settings(this);
savedPassword = settings.getPrivString(Settings.SSH_PASSWORD_KEY);
XHttpSshCore.init(this);
}
String getSessionPassword() {
return savedPassword;
}
void setSessionPassword(String password) {
savedPassword = password == null ? "" : password;
settings.getPrefsPrivate().edit()
.putString(Settings.SSH_PASSWORD_KEY, savedPassword)
.apply();
}
boolean hasSessionPassword() {
return savedPassword != null && !savedPassword.isEmpty();
}
}
@@ -0,0 +1,216 @@
package com.dragonssh.xhttpdemo;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.text.Html;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.recyclerview.widget.RecyclerView;
import com.dragonssh.xhttpdemo.core.logger.LogItem;
import com.dragonssh.xhttpdemo.core.logger.SkStatus;
import com.google.android.material.card.MaterialCardView;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
final class LogAdapter extends RecyclerView.Adapter<LogAdapter.LogViewHolder> {
private static final int MAX_STORED_ENTRIES = 500;
private static final int MAX_VISIBLE_ENTRIES = 250;
private final Context context;
private final List<Entry> allEntries = new ArrayList<>();
private final List<Entry> visibleEntries = new ArrayList<>();
private final SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm:ss.SSS", Locale.US);
private boolean showDebug;
LogAdapter(Context context) {
this.context = context.getApplicationContext();
}
void replace(LogItem[] items) {
allEntries.clear();
if (items != null) {
int start = Math.max(0, items.length - MAX_STORED_ENTRIES);
for (int i = start; i < items.length; i++) {
Entry entry = toEntry(items[i]);
if (entry != null) allEntries.add(entry);
}
}
rebuildVisible();
}
void add(LogItem item) {
Entry entry = toEntry(item);
if (entry == null) return;
allEntries.add(entry);
while (allEntries.size() > MAX_STORED_ENTRIES) allEntries.remove(0);
if (isVisible(entry)) {
visibleEntries.add(entry);
notifyItemInserted(visibleEntries.size() - 1);
if (visibleEntries.size() > MAX_VISIBLE_ENTRIES) {
visibleEntries.remove(0);
notifyItemRemoved(0);
}
}
}
void setShowDebug(boolean showDebug) {
if (this.showDebug == showDebug) return;
this.showDebug = showDebug;
rebuildVisible();
}
void clear() {
int count = visibleEntries.size();
allEntries.clear();
visibleEntries.clear();
if (count > 0) notifyItemRangeRemoved(0, count);
}
boolean isEmpty() {
return visibleEntries.isEmpty();
}
String asPlainText() {
StringBuilder out = new StringBuilder();
for (Entry entry : visibleEntries) {
if (out.length() > 0) out.append('\n');
out.append(entry.time)
.append(" [")
.append(entry.level.name())
.append("] ")
.append(entry.message);
}
return out.toString();
}
@NonNull
@Override
public LogViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_log, parent, false);
return new LogViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull LogViewHolder holder, int position) {
Entry entry = visibleEntries.get(position);
int accent = colorFor(entry.level);
holder.time.setText(entry.time);
holder.level.setText(entry.level.name());
holder.message.setText(entry.message);
holder.card.setStrokeColor(accent);
Drawable background = holder.level.getBackground();
if (background != null) {
Drawable wrapped = DrawableCompat.wrap(background.mutate());
DrawableCompat.setTintList(wrapped, ColorStateList.valueOf(accent));
holder.level.setBackground(wrapped);
}
}
@Override
public int getItemCount() {
return visibleEntries.size();
}
private void rebuildVisible() {
visibleEntries.clear();
for (Entry entry : allEntries) {
if (isVisible(entry)) visibleEntries.add(entry);
}
while (visibleEntries.size() > MAX_VISIBLE_ENTRIES) visibleEntries.remove(0);
notifyDataSetChanged();
}
private boolean isVisible(Entry entry) {
return showDebug
|| (entry.level != SkStatus.LogLevel.DEBUG
&& entry.level != SkStatus.LogLevel.VERBOSE);
}
private Entry toEntry(LogItem item) {
if (item == null) return null;
String raw;
try {
raw = item.getString(context);
} catch (Exception e) {
raw = item.getMessage();
}
if (raw == null) return null;
String clean = Html.fromHtml(raw, Html.FROM_HTML_MODE_LEGACY)
.toString()
.replace('\u00A0', ' ')
.trim();
if (clean.isEmpty()) return null;
long timestamp = item.getLogtime();
SkStatus.LogLevel level = item.getLogLevel() != null
? item.getLogLevel() : SkStatus.LogLevel.INFO;
return new Entry(timeFormat.format(new Date(timestamp)), level, clean);
}
private int colorFor(SkStatus.LogLevel level) {
int resource;
switch (level) {
case ERROR:
resource = R.color.log_error;
break;
case WARNING:
resource = R.color.log_warning;
break;
case DEBUG:
resource = R.color.log_debug;
break;
case VERBOSE:
resource = R.color.log_verbose;
break;
case INFO:
default:
resource = R.color.log_info;
break;
}
return ContextCompat.getColor(context, resource);
}
static final class LogViewHolder extends RecyclerView.ViewHolder {
final MaterialCardView card;
final TextView time;
final TextView level;
final TextView message;
LogViewHolder(@NonNull View itemView) {
super(itemView);
card = itemView.findViewById(R.id.log_card);
time = itemView.findViewById(R.id.log_time);
level = itemView.findViewById(R.id.log_level);
message = itemView.findViewById(R.id.log_message);
}
}
private static final class Entry {
final String time;
final SkStatus.LogLevel level;
final String message;
Entry(String time, SkStatus.LogLevel level, String message) {
this.time = time;
this.level = level;
this.message = message;
}
}
}
@@ -0,0 +1,91 @@
package com.dragonssh.xhttpdemo;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.os.Bundle;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.dragonssh.xhttpdemo.core.logger.LogItem;
import com.dragonssh.xhttpdemo.core.logger.SkStatus;
import com.google.android.material.switchmaterial.SwitchMaterial;
public final class LogsActivity extends AppCompatActivity implements SkStatus.LogListener {
private RecyclerView logList;
private LogAdapter logAdapter;
private SwitchMaterial followLogsSwitch;
private SwitchMaterial showDebugLogsSwitch;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_logs);
WindowInsetsHelper.apply(this, R.id.logs_root);
logList = findViewById(R.id.log_list);
followLogsSwitch = findViewById(R.id.follow_logs);
showDebugLogsSwitch = findViewById(R.id.show_debug_logs);
logAdapter = new LogAdapter(this);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setStackFromEnd(true);
logList.setLayoutManager(layoutManager);
logList.setAdapter(logAdapter);
logList.setItemAnimator(null);
findViewById(R.id.back).setOnClickListener(v -> finish());
findViewById(R.id.copy_logs).setOnClickListener(v -> copyLogs());
findViewById(R.id.clear_logs).setOnClickListener(v -> SkStatus.clearLog());
showDebugLogsSwitch.setOnCheckedChangeListener((button, checked) -> {
logAdapter.setShowDebug(checked);
if (followLogsSwitch.isChecked()) scrollLogsToBottom();
});
}
@Override
protected void onStart() {
super.onStart();
SkStatus.addLogListener(this);
logAdapter.replace(SkStatus.getlogbuffer());
scrollLogsToBottom();
}
@Override
protected void onStop() {
SkStatus.removeLogListener(this);
super.onStop();
}
@Override
public void newLog(LogItem logItem) {
runOnUiThread(() -> {
logAdapter.add(logItem);
if (followLogsSwitch.isChecked()) scrollLogsToBottom();
});
}
@Override
public void onClear() {
runOnUiThread(logAdapter::clear);
}
private void scrollLogsToBottom() {
if (logAdapter == null || logAdapter.getItemCount() == 0) return;
logList.post(() -> logList.scrollToPosition(logAdapter.getItemCount() - 1));
}
private void copyLogs() {
if (logAdapter.isEmpty()) {
Toast.makeText(this, R.string.no_logs_to_copy, Toast.LENGTH_SHORT).show();
return;
}
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setPrimaryClip(ClipData.newPlainText(
"DragonSSH XHTTP logs", logAdapter.asPlainText()));
Toast.makeText(this, R.string.logs_copied, Toast.LENGTH_SHORT).show();
}
}
@@ -0,0 +1,220 @@
package com.dragonssh.xhttpdemo;
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.VpnService;
import android.os.Build;
import android.os.Bundle;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import com.dragonssh.xhttpdemo.core.config.Settings;
import com.dragonssh.xhttpdemo.core.logger.ConnectionStatus;
import com.dragonssh.xhttpdemo.core.logger.SkStatus;
import com.dragonssh.xhttpdemo.core.tunnel.TunnelManagerHelper;
import com.google.android.material.button.MaterialButton;
public final class MainActivity extends AppCompatActivity implements SkStatus.StateListener {
private static final int NOTIFICATION_PERMISSION_REQUEST = 42;
private Settings settings;
private TextView statusView;
private TextView serverValue;
private TextView usernameValue;
private TextView sniValue;
private TextView hostValue;
private TextView pathValue;
private TextView tlsModeValue;
private TextView xhttpTlsValue;
private MaterialButton startStopButton;
private boolean tunnelActive;
private final ActivityResultLauncher<Intent> settingsLauncher =
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> refreshSummary());
private final ActivityResultLauncher<Intent> vpnPermissionLauncher =
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
if (result.getResultCode() == RESULT_OK) {
startTunnel();
} else {
Toast.makeText(this, R.string.vpn_permission_required, Toast.LENGTH_LONG).show();
}
});
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
WindowInsetsHelper.apply(this, R.id.main_root);
settings = new Settings(this);
bindViews();
startStopButton.setOnClickListener(v -> {
if (tunnelActive || SkStatus.isTunnelActive()) {
TunnelManagerHelper.stopXHttpSsh(this);
} else {
requestVpnAndConnect();
}
});
findViewById(R.id.open_settings).setOnClickListener(v -> openSettings());
findViewById(R.id.open_vpn_settings).setOnClickListener(v ->
settingsLauncher.launch(new Intent(this, VpnSettingsActivity.class)));
findViewById(R.id.open_logs).setOnClickListener(v ->
startActivity(new Intent(this, LogsActivity.class)));
refreshSummary();
requestNotificationPermissionIfNeeded();
}
@Override
protected void onStart() {
super.onStart();
SkStatus.addStateListener(this);
refreshSummary();
}
@Override
protected void onStop() {
SkStatus.removeStateListener(this);
super.onStop();
}
private void bindViews() {
statusView = findViewById(R.id.status);
serverValue = findViewById(R.id.summary_server);
usernameValue = findViewById(R.id.summary_username);
sniValue = findViewById(R.id.summary_sni);
hostValue = findViewById(R.id.summary_xhttp_host);
pathValue = findViewById(R.id.summary_xhttp_path);
tlsModeValue = findViewById(R.id.summary_tls_mode);
xhttpTlsValue = findViewById(R.id.summary_xhttp_tls);
startStopButton = findViewById(R.id.start_stop);
}
private void refreshSummary() {
if (settings == null) return;
String server = settings.getPrivString(Settings.XHTTP_ENDPOINT_KEY);
String port = settings.getPrivString(Settings.XHTTP_PORT_KEY);
String username = settings.getPrivString(Settings.USUARIO_KEY);
String sni = settings.getPrivString(Settings.CUSTOM_SNI);
String host = settings.getPrivString(Settings.XHTTP_HOST_KEY);
String path = ConfigUtils.normalizePath(settings.getPrivString(Settings.XHTTP_PATH_KEY));
boolean tls = !"0".equals(settings.getPrivString(Settings.XHTTP_TLS_KEY));
serverValue.setText(ConfigUtils.formatHostPort(server, port));
usernameValue.setText(ConfigUtils.displayValue(username));
sniValue.setText(ConfigUtils.displayValue(sni));
hostValue.setText(ConfigUtils.displayValue(host));
pathValue.setText(ConfigUtils.displayValue(path));
tlsModeValue.setText(tls ? R.string.tls_mode_automatic : R.string.tls_mode_disabled);
xhttpTlsValue.setText(tls ? R.string.enabled : R.string.disabled);
}
private void openSettings() {
settingsLauncher.launch(new Intent(this, SettingsActivity.class));
}
private void requestVpnAndConnect() {
String error = validateSavedConfiguration();
if (error != null) {
Toast.makeText(this, error, Toast.LENGTH_LONG).show();
openSettings();
return;
}
DemoApplication application = (DemoApplication) getApplication();
if (!application.hasSessionPassword()) {
Toast.makeText(this, R.string.password_needed_for_session, Toast.LENGTH_LONG).show();
openSettings();
return;
}
Intent permissionIntent = VpnService.prepare(this);
if (permissionIntent != null) {
vpnPermissionLauncher.launch(permissionIntent);
} else {
startTunnel();
}
}
private String validateSavedConfiguration() {
String server = settings.getPrivString(Settings.XHTTP_ENDPOINT_KEY).trim();
String port = settings.getPrivString(Settings.XHTTP_PORT_KEY).trim();
String username = settings.getPrivString(Settings.USUARIO_KEY).trim();
String path = ConfigUtils.normalizePath(settings.getPrivString(Settings.XHTTP_PATH_KEY));
if (server.isEmpty()) return getString(R.string.error_server_required);
if (!ConfigUtils.isValidPort(port)) return getString(R.string.error_port_invalid);
if (username.isEmpty()) return getString(R.string.error_username_required);
if (path.isEmpty()) return getString(R.string.error_path_required);
if (settings.getVpnDnsForward() && settings.getVpnDnsResolver().trim().isEmpty()) {
return getString(R.string.error_dns_primary_required);
}
if (settings.getVpnUdpForward() && !ConfigUtils.isHostPort(settings.getVpnUdpResolver())) {
return getString(R.string.error_udpgw_invalid);
}
return null;
}
private void startTunnel() {
DemoApplication application = (DemoApplication) getApplication();
String password = application.getSessionPassword();
if (password.isEmpty()) {
Toast.makeText(this, R.string.password_needed_for_session, Toast.LENGTH_LONG).show();
openSettings();
return;
}
String server = settings.getPrivString(Settings.XHTTP_ENDPOINT_KEY);
String port = settings.getPrivString(Settings.XHTTP_PORT_KEY);
String username = settings.getPrivString(Settings.USUARIO_KEY);
String sni = settings.getPrivString(Settings.CUSTOM_SNI);
String host = settings.getPrivString(Settings.XHTTP_HOST_KEY);
String path = ConfigUtils.normalizePath(settings.getPrivString(Settings.XHTTP_PATH_KEY));
boolean tls = !"0".equals(settings.getPrivString(Settings.XHTTP_TLS_KEY));
statusView.setText(R.string.status_starting);
SkStatus.logInfo("Server: " + ConfigUtils.formatHostPort(server, port));
SkStatus.logInfo("XHTTP: SNI=" + ConfigUtils.displayValue(sni)
+ ", Host=" + ConfigUtils.displayValue(host)
+ ", Path=" + ConfigUtils.displayValue(path)
+ ", TLS=" + (tls ? "on" : "off"));
SkStatus.logInfo("SSH login: " + username + " (password hidden)");
TunnelManagerHelper.startXHttpSsh(this, password);
}
@Override
public void updateState(String state, String logMessage, int localizedResId,
ConnectionStatus level, Intent intent) {
runOnUiThread(() -> {
String label;
try {
label = getString(localizedResId);
} catch (Exception e) {
label = state;
}
statusView.setText(label);
tunnelActive = level != ConnectionStatus.LEVEL_NOTCONNECTED
&& level != ConnectionStatus.LEVEL_AUTH_FAILED;
startStopButton.setText(tunnelActive ? R.string.stop : R.string.start);
});
}
private void requestNotificationPermissionIfNeeded() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
&& ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.POST_NOTIFICATIONS},
NOTIFICATION_PERMISSION_REQUEST);
}
}
}
@@ -0,0 +1,132 @@
package com.dragonssh.xhttpdemo;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.widget.EditText;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.dragonssh.xhttpdemo.core.config.Settings;
import com.google.android.material.switchmaterial.SwitchMaterial;
import com.google.android.material.textfield.TextInputLayout;
public final class SettingsActivity extends AppCompatActivity {
private Settings settings;
private EditText serverField;
private EditText portField;
private EditText usernameField;
private EditText passwordField;
private EditText sniField;
private EditText xhttpHostField;
private EditText pathField;
private SwitchMaterial xhttpTlsSwitch;
private TextInputLayout passwordLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
WindowInsetsHelper.apply(this, R.id.settings_root);
settings = new Settings(this);
bindViews();
loadConfiguration();
findViewById(R.id.back).setOnClickListener(v -> finish());
findViewById(R.id.save_settings).setOnClickListener(v -> saveConfiguration());
}
private void bindViews() {
serverField = findViewById(R.id.server);
portField = findViewById(R.id.port);
usernameField = findViewById(R.id.username);
passwordField = findViewById(R.id.password);
sniField = findViewById(R.id.sni);
xhttpHostField = findViewById(R.id.xhttp_host);
pathField = findViewById(R.id.xhttp_path);
xhttpTlsSwitch = findViewById(R.id.xhttp_tls);
passwordLayout = findViewById(R.id.password_layout);
}
private void loadConfiguration() {
serverField.setText(settings.getPrivString(Settings.XHTTP_ENDPOINT_KEY));
portField.setText(settings.getPrivString(Settings.XHTTP_PORT_KEY));
usernameField.setText(settings.getPrivString(Settings.USUARIO_KEY));
sniField.setText(settings.getPrivString(Settings.CUSTOM_SNI));
xhttpHostField.setText(settings.getPrivString(Settings.XHTTP_HOST_KEY));
pathField.setText(settings.getPrivString(Settings.XHTTP_PATH_KEY));
xhttpTlsSwitch.setChecked(!"0".equals(settings.getPrivString(Settings.XHTTP_TLS_KEY)));
DemoApplication application = (DemoApplication) getApplication();
passwordField.setText(application.getSessionPassword());
passwordLayout.setHelperText(application.hasSessionPassword()
? getString(R.string.password_loaded_for_session)
: getString(R.string.password_storage_notice));
}
private void saveConfiguration() {
String server = text(serverField);
String port = text(portField);
String username = text(usernameField);
String password = rawText(passwordField);
String path = ConfigUtils.normalizePath(text(pathField));
if (server.isEmpty()) {
showError(R.string.error_server_required);
serverField.requestFocus();
return;
}
if (!ConfigUtils.isValidPort(port)) {
showError(R.string.error_port_invalid);
portField.requestFocus();
return;
}
if (username.isEmpty()) {
showError(R.string.error_username_required);
usernameField.requestFocus();
return;
}
DemoApplication application = (DemoApplication) getApplication();
if (password.isEmpty()) {
showError(R.string.error_password_required);
passwordField.requestFocus();
return;
}
if (path.isEmpty()) {
showError(R.string.error_path_required);
pathField.requestFocus();
return;
}
SharedPreferences.Editor editor = settings.getPrefsPrivate().edit();
editor.putString(Settings.XHTTP_ENDPOINT_KEY, server)
.putString(Settings.XHTTP_PORT_KEY, port)
.putString(Settings.USUARIO_KEY, username)
.putString(Settings.PORTA_LOCAL_KEY, "1080")
.putString(Settings.CUSTOM_SNI, text(sniField))
.putString(Settings.XHTTP_HOST_KEY, text(xhttpHostField))
.putString(Settings.XHTTP_PATH_KEY, path)
.putString(Settings.XHTTP_TLS_KEY, xhttpTlsSwitch.isChecked() ? "1" : "0")
.apply();
application.setSessionPassword(password);
setResult(RESULT_OK);
Toast.makeText(this, R.string.settings_saved, Toast.LENGTH_SHORT).show();
finish();
}
private void showError(int messageRes) {
Toast.makeText(this, messageRes, Toast.LENGTH_LONG).show();
}
private static String text(EditText view) {
return view.getText() == null ? "" : view.getText().toString().trim();
}
private static String rawText(EditText view) {
return view.getText() == null ? "" : view.getText().toString();
}
}
@@ -0,0 +1,96 @@
package com.dragonssh.xhttpdemo;
import android.os.Bundle;
import android.widget.EditText;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.dragonssh.xhttpdemo.core.config.Settings;
import com.google.android.material.switchmaterial.SwitchMaterial;
public final class VpnSettingsActivity extends AppCompatActivity {
private Settings settings;
private SwitchMaterial customDnsSwitch;
private EditText primaryDnsField;
private EditText secondaryDnsField;
private SwitchMaterial udpGatewaySwitch;
private EditText udpGatewayField;
private SwitchMaterial ipv6Switch;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_vpn_settings);
WindowInsetsHelper.apply(this, R.id.vpn_settings_root);
settings = new Settings(this);
bindViews();
loadConfiguration();
customDnsSwitch.setOnCheckedChangeListener((button, checked) -> updateEnabledState());
udpGatewaySwitch.setOnCheckedChangeListener((button, checked) -> updateEnabledState());
findViewById(R.id.back).setOnClickListener(v -> finish());
findViewById(R.id.save_vpn_settings).setOnClickListener(v -> saveConfiguration());
}
private void bindViews() {
customDnsSwitch = findViewById(R.id.use_custom_dns);
primaryDnsField = findViewById(R.id.dns_primary);
secondaryDnsField = findViewById(R.id.dns_secondary);
udpGatewaySwitch = findViewById(R.id.enable_udpgw);
udpGatewayField = findViewById(R.id.udpgw_address);
ipv6Switch = findViewById(R.id.enable_ipv6);
}
private void loadConfiguration() {
customDnsSwitch.setChecked(settings.getVpnDnsForward());
primaryDnsField.setText(settings.getVpnDnsResolver());
secondaryDnsField.setText(settings.getVpnDnsResolverSecondary());
udpGatewaySwitch.setChecked(settings.getVpnUdpForward());
udpGatewayField.setText(settings.getVpnUdpResolver());
// Switch shows IPv6 ENABLED; the stored flag is the inverse (disableIpv6Tunnel).
ipv6Switch.setChecked(!settings.getDisableIpv6Tunnel());
updateEnabledState();
}
private void updateEnabledState() {
primaryDnsField.setEnabled(customDnsSwitch.isChecked());
secondaryDnsField.setEnabled(customDnsSwitch.isChecked());
udpGatewayField.setEnabled(udpGatewaySwitch.isChecked());
}
private void saveConfiguration() {
String primaryDns = text(primaryDnsField);
String secondaryDns = text(secondaryDnsField);
String udpGateway = text(udpGatewayField);
if (customDnsSwitch.isChecked() && primaryDns.isEmpty()) {
Toast.makeText(this, R.string.error_dns_primary_required, Toast.LENGTH_LONG).show();
primaryDnsField.requestFocus();
return;
}
if (udpGatewaySwitch.isChecked() && !ConfigUtils.isHostPort(udpGateway)) {
Toast.makeText(this, R.string.error_udpgw_invalid, Toast.LENGTH_LONG).show();
udpGatewayField.requestFocus();
return;
}
settings.getVpnPrefs().edit()
.putBoolean(Settings.DNSFORWARD_KEY, customDnsSwitch.isChecked())
.putString(Settings.DNSRESOLVER_KEY, primaryDns)
.putString(Settings.DNSRESOLVER_SECONDARY_KEY, secondaryDns)
.putBoolean(Settings.UDPFORWARD_KEY, udpGatewaySwitch.isChecked())
.putString(Settings.UDPRESOLVER_KEY, udpGateway)
.putBoolean(Settings.DISABLE_IPV6_TUNNEL_KEY, !ipv6Switch.isChecked())
.apply();
setResult(RESULT_OK);
Toast.makeText(this, R.string.settings_saved, Toast.LENGTH_SHORT).show();
finish();
}
private static String text(EditText view) {
return view.getText() == null ? "" : view.getText().toString().trim();
}
}
@@ -0,0 +1,39 @@
package com.dragonssh.xhttpdemo;
import android.app.Activity;
import android.view.View;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
final class WindowInsetsHelper {
private WindowInsetsHelper() { }
static void apply(Activity activity, int rootViewId) {
WindowCompat.setDecorFitsSystemWindows(activity.getWindow(), false);
View root = activity.findViewById(rootViewId);
if (root == null) return;
final int initialLeft = root.getPaddingLeft();
final int initialTop = root.getPaddingTop();
final int initialRight = root.getPaddingRight();
final int initialBottom = root.getPaddingBottom();
ViewCompat.setOnApplyWindowInsetsListener(root, (view, windowInsets) -> {
Insets systemBars = windowInsets.getInsets(
WindowInsetsCompat.Type.systemBars()
| WindowInsetsCompat.Type.displayCutout());
Insets ime = windowInsets.getInsets(WindowInsetsCompat.Type.ime());
view.setPadding(
initialLeft + systemBars.left,
initialTop + systemBars.top,
initialRight + systemBars.right,
initialBottom + Math.max(systemBars.bottom, ime.bottom));
return windowInsets;
});
ViewCompat.requestApplyInsets(root);
}
}
@@ -0,0 +1,8 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:fillColor="#5B5BD6" android:pathData="M0,0h108v108h-108z" />
<path android:fillColor="#FFFFFF" android:pathData="M22,29h64v12h-64zM22,48h42v12h-42zM22,67h64v12h-64z" />
</vector>
@@ -0,0 +1,5 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/log_surface" />
<stroke android:width="1dp" android:color="@color/log_stroke" />
<corners android:radius="12dp" />
</shape>
@@ -0,0 +1,4 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/log_debug" />
<corners android:radius="999dp" />
</shape>
+82
View File
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/logs_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/back"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:minWidth="0dp"
android:text="@string/back_arrow"
android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:text="@string/logs"
android:textAppearance="@style/TextAppearance.Material3.HeadlineMedium"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/copy_logs"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/copy_logs" />
<com.google.android.material.button.MaterialButton
android:id="@+id/clear_logs"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:text="@string/clear_logs" />
</LinearLayout>
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/follow_logs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:checked="true"
android:text="@string/follow_logs" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/show_debug_logs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="false"
android:text="@string/show_debug_logs" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/log_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:layout_weight="1"
android:background="@drawable/log_background"
android:clipToPadding="false"
android:padding="8dp" />
</LinearLayout>
+227
View File
@@ -0,0 +1,227 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:fillViewport="true"
android:padding="20dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textAppearance="@style/TextAppearance.Material3.HeadlineMedium"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/home_subtitle"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium" />
<com.google.android.material.button.MaterialButton
android:id="@+id/start_stop"
android:layout_width="match_parent"
android:layout_height="64dp"
android:layout_marginTop="32dp"
android:text="@string/start"
android:textAppearance="@style/TextAppearance.Material3.TitleLarge"
android:textStyle="bold"
app:cornerRadius="14dp" />
<TextView
android:id="@+id/status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="center"
android:text="@string/status_disconnected"
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="28dp"
android:gravity="center"
android:text="@string/connection_method"
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
android:textStyle="bold" />
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
app:cardCornerRadius="14dp"
app:strokeWidth="1dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="18dp"
android:text="@string/xhttp_ssh"
android:textAppearance="@style/TextAppearance.Material3.TitleLarge"
android:textStyle="bold" />
</com.google.android.material.card.MaterialCardView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/open_settings"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/settings" />
<com.google.android.material.button.MaterialButton
android:id="@+id/open_vpn_settings"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:text="@string/vpn_udpgw" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/open_logs"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/logs" />
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginBottom="12dp"
app:cardCornerRadius="14dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="18dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/current_configuration"
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="18dp"
android:text="@string/server"
android:textStyle="bold" />
<TextView
android:id="@+id/summary_server"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="—"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:text="@string/username"
android:textStyle="bold" />
<TextView
android:id="@+id/summary_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="—"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:text="@string/sni"
android:textStyle="bold" />
<TextView
android:id="@+id/summary_sni"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="—"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:text="@string/xhttp_host"
android:textStyle="bold" />
<TextView
android:id="@+id/summary_xhttp_host"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="—"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:text="@string/xhttp_path"
android:textStyle="bold" />
<TextView
android:id="@+id/summary_xhttp_path"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="—"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:text="@string/tls_mode"
android:textStyle="bold" />
<TextView
android:id="@+id/summary_tls_mode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/tls_mode_automatic"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:text="@string/xhttp_tls"
android:textStyle="bold" />
<TextView
android:id="@+id/summary_xhttp_tls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/enabled"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
@@ -0,0 +1,245 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/settings_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:fillViewport="true"
android:padding="20dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/back"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:minWidth="0dp"
android:text="@string/back_arrow"
android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:text="@string/settings"
android:textAppearance="@style/TextAppearance.Material3.HeadlineMedium"
android:textStyle="bold" />
</LinearLayout>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
app:cardCornerRadius="14dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="18dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/ssh_tunnel"
android:textAppearance="@style/TextAppearance.Material3.TitleLarge"
android:textStyle="bold" />
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:hint="@string/server"
app:helperText="@string/server_help">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/server"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:hint="@string/port">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/port"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
app:cardCornerRadius="14dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="18dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/authentication"
android:textAppearance="@style/TextAppearance.Material3.TitleLarge"
android:textStyle="bold" />
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:hint="@string/username">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/password_layout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:hint="@string/password"
app:endIconMode="password_toggle">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
app:cardCornerRadius="14dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="18dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/xhttp"
android:textAppearance="@style/TextAppearance.Material3.TitleLarge"
android:textStyle="bold" />
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:hint="@string/sni"
app:helperText="@string/sni_help_simple">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/sni"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:hint="@string/xhttp_host"
app:helperText="@string/xhttp_host_help_simple">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/xhttp_host"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:hint="@string/xhttp_path"
app:helperText="@string/xhttp_path_help_simple">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/xhttp_path"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:text="@string/tls_mode"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/tls_mode_automatic"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/xhttp_tls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/xhttp_tls" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.button.MaterialButton
android:id="@+id/save_settings"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginTop="18dp"
android:layout_marginBottom="12dp"
android:text="@string/save"
android:textStyle="bold"
app:cornerRadius="14dp" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
@@ -0,0 +1,188 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/vpn_settings_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:fillViewport="true"
android:padding="20dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/back"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:minWidth="0dp"
android:text="@string/back_arrow"
android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:text="@string/vpn_udpgw"
android:textAppearance="@style/TextAppearance.Material3.HeadlineMedium"
android:textStyle="bold" />
</LinearLayout>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
app:cardCornerRadius="14dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="18dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/dns"
android:textAppearance="@style/TextAppearance.Material3.TitleLarge"
android:textStyle="bold" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/use_custom_dns"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/use_custom_dns" />
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:hint="@string/dns_1">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/dns_primary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:hint="@string/dns_2">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/dns_secondary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
app:cardCornerRadius="14dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="18dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/udpgw"
android:textAppearance="@style/TextAppearance.Material3.TitleLarge"
android:textStyle="bold" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/enable_udpgw"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/enable_udpgw" />
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:hint="@string/udpgw_address"
app:helperText="@string/udpgw_help_simple">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/udpgw_address"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
app:cardCornerRadius="14dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="18dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/ipv6"
android:textAppearance="@style/TextAppearance.Material3.TitleLarge"
android:textStyle="bold" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/enable_ipv6"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/enable_ipv6" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/ipv6_help"
android:textAppearance="@style/TextAppearance.Material3.BodySmall" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.button.MaterialButton
android:id="@+id/save_vpn_settings"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginTop="18dp"
android:layout_marginBottom="12dp"
android:text="@string/save"
android:textStyle="bold"
app:cornerRadius="14dp" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
+60
View File
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/log_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="6dp"
app:cardBackgroundColor="@color/log_row_surface"
app:cardCornerRadius="10dp"
app:cardElevation="0dp"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="10dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/log_time"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:fontFamily="monospace"
android:textColor="@color/log_secondary_text"
android:textSize="11sp" />
<TextView
android:id="@+id/log_level"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/log_level_badge"
android:fontFamily="sans"
android:paddingStart="8dp"
android:paddingTop="2dp"
android:paddingEnd="8dp"
android:paddingBottom="2dp"
android:textColor="@android:color/white"
android:textSize="10sp"
android:textStyle="bold" />
</LinearLayout>
<TextView
android:id="@+id/log_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:fontFamily="monospace"
android:lineSpacingExtra="2dp"
android:textColor="@color/log_primary_text"
android:textIsSelectable="true"
android:textSize="12sp" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
+14
View File
@@ -0,0 +1,14 @@
<resources>
<color name="seed">#159FE6</color>
<color name="log_surface">#101216</color>
<color name="log_row_surface">#171A20</color>
<color name="preview_surface">#171A20</color>
<color name="log_stroke">#343945</color>
<color name="log_primary_text">#F2F4F8</color>
<color name="log_secondary_text">#AEB6C4</color>
<color name="log_info">#3F8CFF</color>
<color name="log_warning">#D68A00</color>
<color name="log_error">#D94A4A</color>
<color name="log_debug">#687386</color>
<color name="log_verbose">#7C5BD6</color>
</resources>
+70
View File
@@ -0,0 +1,70 @@
<resources>
<string name="app_name">DragonSSH XHTTP Example</string>
<string name="home_subtitle">Minimal XHTTP + SSH client with UDPGW.</string>
<string name="start">START</string>
<string name="stop">STOP</string>
<string name="connection_method">Connection Method</string>
<string name="xhttp_ssh">XHTTP + SSH</string>
<string name="settings">Settings</string>
<string name="vpn_udpgw">VPN / UDPGW</string>
<string name="logs">Logs</string>
<string name="current_configuration">Current configuration</string>
<string name="ssh_tunnel">SSH Tunnel</string>
<string name="authentication">Authentication</string>
<string name="xhttp">XHTTP</string>
<string name="server">Server</string>
<string name="server_help">The XHTTP proxy IP or hostname.</string>
<string name="port">Port</string>
<string name="username">User name</string>
<string name="password">Password</string>
<string name="password_storage_notice">The password will be saved in private app storage.</string>
<string name="password_loaded_for_session">The saved password is shown masked. Edit it to replace it.</string>
<string name="sni">SNI</string>
<string name="sni_help_simple">TLS hostname, for example app.example.com.</string>
<string name="xhttp_host">XHTTP Host</string>
<string name="xhttp_host_help_simple">Host used by your XHTTP server or CDN.</string>
<string name="xhttp_path">XHTTP Path</string>
<string name="xhttp_path_help_simple">Example: /ssh</string>
<string name="tls_mode">TLS MODE</string>
<string name="tls_mode_automatic">Automatic (TLS 1.3 preferred)</string>
<string name="tls_mode_disabled">Disabled</string>
<string name="xhttp_tls">XHTTP TLS</string>
<string name="enabled">Enabled</string>
<string name="disabled">Disabled</string>
<string name="save">SAVE</string>
<string name="back_arrow"></string>
<string name="dns">DNS</string>
<string name="use_custom_dns">Use custom DNS</string>
<string name="dns_1">DNS 1</string>
<string name="dns_2">DNS 2</string>
<string name="udpgw">UDPGW</string>
<string name="enable_udpgw">Enable UDPGW</string>
<string name="udpgw_address">UDPGW host:port</string>
<string name="udpgw_help_simple">Example: 127.0.0.1:7300</string>
<string name="ipv6">IPv6</string>
<string name="enable_ipv6">Enable IPv6 tunnel</string>
<string name="ipv6_help">Route IPv6 through the tunnel. Leave off if your server or network has no working IPv6.</string>
<string name="copy_logs">Copy</string>
<string name="clear_logs">Clear</string>
<string name="follow_logs">Follow newest entries</string>
<string name="show_debug_logs">Show DEBUG and VERBOSE</string>
<string name="logs_copied">Logs copied to the clipboard.</string>
<string name="no_logs_to_copy">There are no logs to copy.</string>
<string name="settings_saved">Settings saved.</string>
<string name="status_disconnected">Disconnected</string>
<string name="status_starting">Starting…</string>
<string name="vpn_permission_required">VPN permission is required to connect.</string>
<string name="password_needed_for_session">Open Settings and save the SSH password.</string>
<string name="error_server_required">Enter the Server. This is the XHTTP proxy IP or hostname.</string>
<string name="error_port_invalid">Enter a valid port from 1 to 65535.</string>
<string name="error_username_required">Enter the SSH user name.</string>
<string name="error_password_required">Enter the SSH password.</string>
<string name="error_path_required">Enter the XHTTP Path.</string>
<string name="error_dns_primary_required">Enter DNS 1 or disable custom DNS.</string>
<string name="error_udpgw_invalid">Enter UDPGW as host:port.</string>
</resources>
+11
View File
@@ -0,0 +1,11 @@
<resources>
<style name="Theme.DragonSshXHttpDemo" parent="Theme.Material3.Dark.NoActionBar">
<item name="colorPrimary">@color/seed</item>
<item name="android:fontFamily">sans</item>
<item name="android:windowBackground">@android:color/black</item>
<item name="android:statusBarColor">@android:color/black</item>
<item name="android:navigationBarColor">@android:color/black</item>
<item name="android:windowLightStatusBar">false</item>
<item name="android:windowLightNavigationBar">false</item>
</style>
</resources>