Launch
This commit is contained in:
@@ -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'
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
# Public example: no custom obfuscation rules required.
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user