diff --git a/CHANGELOG.md b/CHANGELOG.md
index fd0c1ce..ee28a22 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,15 @@ All notable changes to v2rayF are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [1.1.7] - 2026-06-28
+
+### Fixed
+
+- Android connect crash — Xray now starts via Java `ProcessBuilder` instead of `System.Diagnostics.Process` (fixes force-close on Samsung and other devices)
+- Android connect — proxy core resolves the process host at runtime so the Android host is always used
+- Android connect — status and busy state updates are marshalled to the UI thread after every async step
+- Latency tests on Android use the same Java process launcher (no .NET `Process` on mobile)
+
## [1.1.6] - 2026-06-28
### Fixed
diff --git a/docs/releases/v1.1.7.md b/docs/releases/v1.1.7.md
new file mode 100644
index 0000000..25b973d
--- /dev/null
+++ b/docs/releases/v1.1.7.md
@@ -0,0 +1,44 @@
+# v2rayF v1.1.7
+
+Critical Android connect crash fix for Samsung Galaxy M21 and similar devices.
+
+---
+
+## Highlights
+
+- Fixes app force-close when tapping **Connect** on Android 12+
+- Xray core now starts through Android's native `ProcessBuilder` (not .NET `Process`)
+- Connection errors show in the app instead of silently crashing
+
+---
+
+## Downloads
+
+| Platform | Package | How to run |
+|----------|---------|------------|
+| Android ARM64 | `v2rayF-android-arm64.zip` | Install `v2rayF-android-arm64.apk` |
+
+Uninstall v1.1.6 first if Connect still crashes, then install this APK.
+
+---
+
+## Fixed
+
+- Connect crash from `System.Diagnostics.Process.Start` on Android (Galaxy M21, Android 12)
+- Wrong process host used if core service initialized before Android app setup
+- UI property updates after `await` running off the main thread on mobile
+- Speedtest/latency on Android using the same unsafe .NET process API
+
+---
+
+## Safe connect (poor networks)
+
+1. **Check** — Xray starts locally without VPN (your normal internet is untouched)
+2. **VPN** — only enabled after the core is verified
+3. **Fail** — VPN is removed immediately if anything goes wrong; you see an error message
+
+---
+
+## Full changelog
+
+[CHANGELOG.md](https://github.com/drmikecrypto/v2rayF/blob/main/CHANGELOG.md)
diff --git a/src/v2rayF.Android/Application.cs b/src/v2rayF.Android/Application.cs
index c199ebd..e2d7e1f 100644
--- a/src/v2rayF.Android/Application.cs
+++ b/src/v2rayF.Android/Application.cs
@@ -21,6 +21,9 @@ public override void OnCreate()
{
AppServices.CoreEnvironment = new AndroidCoreEnvironment();
AppServices.Platform = new AndroidPlatformIntegration();
+ AppServices.CoreProcessHost = new AndroidJavaCoreProcessHost();
+ AndroidEnvironment.UnhandledExceptionRaiser += (_, e) =>
+ global::Android.Util.Log.Error("v2rayF", e.Exception?.ToString() ?? "Unhandled exception");
base.OnCreate();
_ = WarmupAsync();
}
diff --git a/src/v2rayF.Android/Services/AndroidCoreEnvironment.cs b/src/v2rayF.Android/Services/AndroidCoreEnvironment.cs
index 6959cc0..f95e5e1 100644
--- a/src/v2rayF.Android/Services/AndroidCoreEnvironment.cs
+++ b/src/v2rayF.Android/Services/AndroidCoreEnvironment.cs
@@ -63,4 +63,6 @@ private static async Task ExtractAssetIfMissingAsync(string assetName, string de
await using var output = File.Create(destPath);
await input.CopyToAsync(output, cancellationToken).ConfigureAwait(false);
}
+
+ public ICoreProcessHost CreateProcessHost() => new AndroidJavaCoreProcessHost();
}
diff --git a/src/v2rayF.Android/Services/AndroidJavaCoreProcessHost.cs b/src/v2rayF.Android/Services/AndroidJavaCoreProcessHost.cs
new file mode 100644
index 0000000..e40443e
--- /dev/null
+++ b/src/v2rayF.Android/Services/AndroidJavaCoreProcessHost.cs
@@ -0,0 +1,142 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Java.IO;
+using Java.Lang;
+using v2rayF.Services;
+using Process = Java.Lang.Process;
+
+namespace v2rayF.Android.Services;
+
+///
+/// Starts Xray via Android ProcessBuilder — System.Diagnostics.Process crashes on many devices.
+///
+public sealed class AndroidJavaCoreProcessHost : ICoreProcessHost
+{
+ private readonly object _lock = new();
+ private Process? _process;
+ private string _recentOutput = "";
+
+ public bool IsRunning
+ {
+ get
+ {
+ lock (_lock)
+ return _process is { IsAlive: true };
+ }
+ }
+
+ public bool HasExited
+ {
+ get
+ {
+ lock (_lock)
+ return _process is null || !_process.IsAlive;
+ }
+ }
+
+ public Task StartAsync(
+ string corePath,
+ string configPath,
+ string workingDirectory,
+ CancellationToken cancellationToken = default)
+ {
+ StopAsync(cancellationToken).GetAwaiter().GetResult();
+
+ lock (_lock)
+ _recentOutput = "";
+
+ var cmd = new[] { corePath, "run", "-c", configPath };
+ var builder = new ProcessBuilder(cmd);
+ builder.Directory(new File(workingDirectory));
+ builder.RedirectErrorStream(true);
+
+ Process process;
+ lock (_lock)
+ {
+ _process = builder.Start();
+ process = _process;
+ }
+
+ if (process is not null)
+ _ = Task.Run(() => DrainOutputAsync(process), cancellationToken);
+
+ return Task.CompletedTask;
+ }
+
+ public Task StopAsync(CancellationToken cancellationToken = default)
+ {
+ lock (_lock)
+ {
+ if (_process is null)
+ return Task.CompletedTask;
+
+ try
+ {
+ if (_process.IsAlive)
+ _process.Destroy();
+ }
+ catch
+ {
+ // Best effort shutdown.
+ }
+ finally
+ {
+ _process = null;
+ }
+ }
+
+ return Task.CompletedTask;
+ }
+
+ public string GetRecentError()
+ {
+ lock (_lock)
+ {
+ if (_process is { IsAlive: true })
+ return _recentOutput.Trim();
+
+ if (_process is not null)
+ {
+ try
+ {
+ var code = _process.ExitValue();
+ var output = _recentOutput.Trim();
+ return string.IsNullOrEmpty(output)
+ ? $"Xray exited with code {code}"
+ : output;
+ }
+ catch (IllegalThreadStateException)
+ {
+ return _recentOutput.Trim();
+ }
+ }
+
+ return _recentOutput.Trim();
+ }
+ }
+
+ private void DrainOutputAsync(Process process)
+ {
+ try
+ {
+ using var reader = new BufferedReader(new InputStreamReader(process.InputStream));
+ string? line;
+ while ((line = reader.ReadLine()) is not null)
+ {
+ lock (_lock)
+ {
+ if (_recentOutput.Length > 0)
+ _recentOutput += '\n';
+ _recentOutput += line;
+ if (_recentOutput.Length > 8192)
+ _recentOutput = _recentOutput[^4096..];
+ }
+ }
+ }
+ catch
+ {
+ // Process ended.
+ }
+ }
+}
diff --git a/src/v2rayF.Core/Services/AppServices.cs b/src/v2rayF.Core/Services/AppServices.cs
index 1385173..cac3c50 100644
--- a/src/v2rayF.Core/Services/AppServices.cs
+++ b/src/v2rayF.Core/Services/AppServices.cs
@@ -9,6 +9,8 @@ public static class AppServices
public static IPlatformIntegration Platform { get; set; } = null!;
+ public static ICoreProcessHost CoreProcessHost { get; set; } = new ManagedCoreProcessHost();
+
/// Called when the Android activity stops — tear down VPN so network is not left hijacked.
public static Func? EmergencyDisconnectAsync { get; set; }
}
diff --git a/src/v2rayF.Core/Services/ICoreEnvironment.cs b/src/v2rayF.Core/Services/ICoreEnvironment.cs
index 97ae6c7..dffbe84 100644
--- a/src/v2rayF.Core/Services/ICoreEnvironment.cs
+++ b/src/v2rayF.Core/Services/ICoreEnvironment.cs
@@ -12,4 +12,6 @@ public interface ICoreEnvironment
string GetCoresDirectory();
string GetDataDirectory();
+
+ ICoreProcessHost CreateProcessHost();
}
diff --git a/src/v2rayF.Core/Services/ICoreProcessHost.cs b/src/v2rayF.Core/Services/ICoreProcessHost.cs
new file mode 100644
index 0000000..3227923
--- /dev/null
+++ b/src/v2rayF.Core/Services/ICoreProcessHost.cs
@@ -0,0 +1,21 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace v2rayF.Services;
+
+public interface ICoreProcessHost
+{
+ bool IsRunning { get; }
+
+ bool HasExited { get; }
+
+ Task StartAsync(
+ string corePath,
+ string configPath,
+ string workingDirectory,
+ CancellationToken cancellationToken = default);
+
+ Task StopAsync(CancellationToken cancellationToken = default);
+
+ string GetRecentError();
+}
diff --git a/src/v2rayF.Core/Services/LatencyService.cs b/src/v2rayF.Core/Services/LatencyService.cs
index 98d4cc5..0e9ef36 100644
--- a/src/v2rayF.Core/Services/LatencyService.cs
+++ b/src/v2rayF.Core/Services/LatencyService.cs
@@ -19,6 +19,7 @@ public sealed class LatencyService
];
private readonly ICoreEnvironment _environment;
+ private readonly ICoreProcessHost _speedtestHost;
private readonly SemaphoreSlim _speedtestLock = new(1, 1);
public const int TimeoutMs = 10000;
@@ -26,6 +27,7 @@ public sealed class LatencyService
public LatencyService(ICoreEnvironment environment)
{
_environment = environment;
+ _speedtestHost = environment.CreateProcessHost();
}
public async Task MeasureAsync(ProxyServer server, CancellationToken cancellationToken = default)
@@ -52,7 +54,6 @@ public LatencyService(ICoreEnvironment environment)
private async Task MeasureViaCoreAsync(ProxyServer server, CancellationToken cancellationToken)
{
await _speedtestLock.WaitAsync(cancellationToken).ConfigureAwait(false);
- Process? process = null;
try
{
var configDir = Path.Combine(_environment.GetDataDirectory(), "runtime");
@@ -63,12 +64,18 @@ await File.WriteAllTextAsync(
XrayConfigBuilder.BuildSpeedtest(server),
cancellationToken).ConfigureAwait(false);
- process = StartCore(configPath);
- if (process is null)
+ var corePath = _environment.GetCorePath();
+ if (!File.Exists(corePath))
return null;
- await WaitForCoreReadyAsync(process, cancellationToken).ConfigureAwait(false);
- if (process.HasExited)
+ await _speedtestHost.StartAsync(
+ corePath,
+ configPath,
+ _environment.GetCoresDirectory(),
+ cancellationToken).ConfigureAwait(false);
+
+ await WaitForCoreReadyAsync(cancellationToken).ConfigureAwait(false);
+ if (_speedtestHost.HasExited)
return -1;
return await ProbeThroughSocksAsync(XrayConfigBuilder.SpeedtestSocksPort, cancellationToken)
@@ -76,39 +83,16 @@ await File.WriteAllTextAsync(
}
finally
{
- await StopCoreAsync(process).ConfigureAwait(false);
+ await _speedtestHost.StopAsync(cancellationToken).ConfigureAwait(false);
_speedtestLock.Release();
}
}
- private static Process? StartCore(string configPath)
- {
- var corePath = AppServices.CoreEnvironment.GetCorePath();
- if (!File.Exists(corePath))
- return null;
-
- var process = CoreProcessLauncher.CreateProcess(
- corePath,
- configPath,
- AppServices.CoreEnvironment.GetCoresDirectory());
-
- try
- {
- CoreProcessLauncher.Start(process);
- return process;
- }
- catch
- {
- process.Dispose();
- return null;
- }
- }
-
- private static async Task WaitForCoreReadyAsync(Process process, CancellationToken cancellationToken)
+ private async Task WaitForCoreReadyAsync(CancellationToken cancellationToken)
{
for (var i = 0; i < 20; i++)
{
- if (process.HasExited)
+ if (_speedtestHost.HasExited)
return;
if (await IsPortOpenAsync("127.0.0.1", XrayConfigBuilder.SpeedtestSocksPort, cancellationToken)
@@ -137,37 +121,6 @@ private static async Task IsPortOpenAsync(string host, int port, Cancellat
}
}
- private static async Task StopCoreAsync(Process? process)
- {
- if (process is null)
- return;
-
- try
- {
- if (!process.HasExited)
- {
- CoreProcessLauncher.Kill(process);
- using var timeout = new CancellationTokenSource(2000);
- try
- {
- await process.WaitForExitAsync(timeout.Token).ConfigureAwait(false);
- }
- catch (OperationCanceledException)
- {
- // Timed out waiting for exit.
- }
- }
- }
- catch
- {
- // Best effort shutdown.
- }
- finally
- {
- process.Dispose();
- }
- }
-
private static async Task ProbeThroughSocksAsync(int socksPort, CancellationToken cancellationToken)
{
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
diff --git a/src/v2rayF.Core/Services/ManagedCoreProcessHost.cs b/src/v2rayF.Core/Services/ManagedCoreProcessHost.cs
new file mode 100644
index 0000000..34ca9c1
--- /dev/null
+++ b/src/v2rayF.Core/Services/ManagedCoreProcessHost.cs
@@ -0,0 +1,134 @@
+using System;
+using System.Diagnostics;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace v2rayF.Services;
+
+public sealed class ManagedCoreProcessHost : ICoreProcessHost
+{
+ private readonly object _stderrLock = new();
+ private readonly StringBuilder _recentStderr = new();
+ private Process? _process;
+ private bool _manualStop;
+
+ public bool IsRunning => _process is { HasExited: false };
+
+ public bool HasExited => _process is null || _process.HasExited;
+
+ public Task StartAsync(
+ string corePath,
+ string configPath,
+ string workingDirectory,
+ CancellationToken cancellationToken = default)
+ {
+ StopAsync(cancellationToken).GetAwaiter().GetResult();
+
+ lock (_stderrLock)
+ {
+ _recentStderr.Clear();
+ }
+
+ _process = CoreProcessLauncher.CreateProcess(corePath, configPath, workingDirectory);
+ _process.ErrorDataReceived += OnErrorDataReceived;
+ _process.Exited += OnProcessExited;
+ _manualStop = false;
+
+ CoreProcessLauncher.Start(_process, line =>
+ {
+ lock (_stderrLock)
+ {
+ if (_recentStderr.Length > 0)
+ _recentStderr.AppendLine();
+ _recentStderr.Append(line);
+ }
+ });
+
+ _ = DrainOutputAsync(_process);
+ return Task.CompletedTask;
+ }
+
+ public async Task StopAsync(CancellationToken cancellationToken = default)
+ {
+ var process = Interlocked.Exchange(ref _process, null);
+ if (process is null)
+ return;
+
+ _manualStop = true;
+ try
+ {
+ if (!process.HasExited)
+ {
+ CoreProcessLauncher.Kill(process);
+ using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+ timeout.CancelAfter(3000);
+ try
+ {
+ await process.WaitForExitAsync(timeout.Token).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ // Timed out waiting for exit.
+ }
+ }
+ }
+ catch
+ {
+ // Best effort shutdown.
+ }
+ finally
+ {
+ process.ErrorDataReceived -= OnErrorDataReceived;
+ process.Exited -= OnProcessExited;
+ process.Dispose();
+ _manualStop = false;
+ }
+ }
+
+ public string GetRecentError()
+ {
+ lock (_stderrLock)
+ {
+ return _recentStderr.ToString().Trim();
+ }
+ }
+
+ private void OnProcessExited(object? sender, EventArgs e)
+ {
+ if (_manualStop)
+ return;
+
+ _process = null;
+ }
+
+ private void OnErrorDataReceived(object sender, DataReceivedEventArgs e)
+ {
+ if (string.IsNullOrEmpty(e.Data))
+ return;
+
+ lock (_stderrLock)
+ {
+ if (_recentStderr.Length > 0)
+ _recentStderr.AppendLine();
+ _recentStderr.Append(e.Data);
+ }
+ }
+
+ private static async Task DrainOutputAsync(Process process)
+ {
+ try
+ {
+ while (!process.HasExited)
+ {
+ var line = await process.StandardOutput.ReadLineAsync().ConfigureAwait(false);
+ if (line is null)
+ break;
+ }
+ }
+ catch
+ {
+ // Process ended.
+ }
+ }
+}
diff --git a/src/v2rayF.Core/Services/ProxyCoreService.cs b/src/v2rayF.Core/Services/ProxyCoreService.cs
index 6dc00e1..764641a 100644
--- a/src/v2rayF.Core/Services/ProxyCoreService.cs
+++ b/src/v2rayF.Core/Services/ProxyCoreService.cs
@@ -1,8 +1,6 @@
using System;
-using System.Diagnostics;
using System.IO;
using System.Net.Sockets;
-using System.Text;
using System.Threading;
using System.Threading.Tasks;
using v2rayF.Models;
@@ -12,18 +10,16 @@ namespace v2rayF.Services;
public sealed class ProxyCoreService : IAsyncDisposable
{
private readonly ICoreEnvironment _environment;
- private readonly object _stderrLock = new();
- private readonly StringBuilder _recentStderr = new();
- private Process? _process;
private string? _configPath;
- private bool _manualStop;
+
+ private static ICoreProcessHost ProcessHost => AppServices.CoreProcessHost;
public ProxyCoreService(ICoreEnvironment environment)
{
_environment = environment;
}
- public bool IsRunning => _process is { HasExited: false };
+ public bool IsRunning => ProcessHost.IsRunning;
public ProxyServer? ActiveServer { get; private set; }
@@ -66,42 +62,21 @@ public async Task StartAsync(ProxyServer server, AppSettings settings, int? tunF
_configPath = Path.Combine(configDir, "config.json");
await File.WriteAllTextAsync(_configPath, configJson, cancellationToken).ConfigureAwait(false);
- lock (_stderrLock)
- {
- _recentStderr.Clear();
- }
+ await ProcessHost.StartAsync(
+ ResolveCorePath(),
+ _configPath,
+ ResolveCoresDirectory(),
+ cancellationToken).ConfigureAwait(false);
- var corePath = ResolveCorePath();
- _process = CoreProcessLauncher.CreateProcess(corePath, _configPath, ResolveCoresDirectory());
+ await WaitForCoreReadyAsync(cancellationToken).ConfigureAwait(false);
- if (!CoreProcessLauncher.IsAndroid)
+ if (ProcessHost.HasExited)
{
- _process.ErrorDataReceived += OnErrorDataReceived;
- _process.Exited += OnProcessExited;
- }
-
- CoreProcessLauncher.Start(_process, line =>
- {
- lock (_stderrLock)
- {
- if (_recentStderr.Length > 0)
- _recentStderr.AppendLine();
- _recentStderr.Append(line);
- }
- });
-
- if (!CoreProcessLauncher.IsAndroid)
- _ = DrainOutputAsync(_process);
-
- await WaitForCoreReadyAsync(_process, cancellationToken).ConfigureAwait(false);
-
- if (_process.HasExited)
- {
- var error = GetRecentStderr();
- CleanupProcess(notify: false);
+ var error = ProcessHost.GetRecentError();
+ await StopAsync(cancellationToken).ConfigureAwait(false);
throw new InvalidOperationException(
string.IsNullOrWhiteSpace(error)
- ? CoreProcessLauncher.FormatAndroidStartFailure()
+ ? "Xray core exited immediately after start."
: FormatStartupError(error));
}
@@ -111,75 +86,18 @@ public async Task StartAsync(ProxyServer server, AppSettings settings, int? tunF
public async Task StopAsync(CancellationToken cancellationToken = default)
{
- var process = Interlocked.Exchange(ref _process, null);
- if (process is null)
- return;
-
- _manualStop = true;
- try
- {
- if (!process.HasExited)
- {
- CoreProcessLauncher.Kill(process);
- using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
- timeout.CancelAfter(3000);
- try
- {
- await process.WaitForExitAsync(timeout.Token).ConfigureAwait(false);
- }
- catch (OperationCanceledException)
- {
- // Timed out waiting for exit.
- }
- }
- }
- catch
- {
- // Best effort shutdown.
- }
- finally
- {
- if (!CoreProcessLauncher.IsAndroid)
- {
- process.ErrorDataReceived -= OnErrorDataReceived;
- process.Exited -= OnProcessExited;
- }
- process.Dispose();
- ActiveServer = null;
- RunningStateChanged?.Invoke(this, false);
- _manualStop = false;
- }
- }
-
- public async ValueTask DisposeAsync() => await StopAsync().ConfigureAwait(false);
-
- private void OnProcessExited(object? sender, EventArgs e)
- {
- if (_manualStop)
- return;
-
+ await ProcessHost.StopAsync(cancellationToken).ConfigureAwait(false);
ActiveServer = null;
RunningStateChanged?.Invoke(this, false);
}
- private void OnErrorDataReceived(object sender, DataReceivedEventArgs e)
- {
- if (string.IsNullOrEmpty(e.Data))
- return;
-
- lock (_stderrLock)
- {
- if (_recentStderr.Length > 0)
- _recentStderr.AppendLine();
- _recentStderr.Append(e.Data);
- }
- }
+ public async ValueTask DisposeAsync() => await StopAsync().ConfigureAwait(false);
- private static async Task WaitForCoreReadyAsync(Process process, CancellationToken cancellationToken)
+ private async Task WaitForCoreReadyAsync(CancellationToken cancellationToken)
{
for (var i = 0; i < 40; i++)
{
- if (process.HasExited)
+ if (ProcessHost.HasExited)
return;
if (await IsPortOpenAsync("127.0.0.1", XrayConfigBuilder.SocksPort, cancellationToken).ConfigureAwait(false))
@@ -207,46 +125,6 @@ private static async Task IsPortOpenAsync(string host, int port, Cancellat
}
}
- private static async Task DrainOutputAsync(Process process)
- {
- try
- {
- while (!process.HasExited)
- {
- var line = await process.StandardOutput.ReadLineAsync().ConfigureAwait(false);
- if (line is null)
- break;
- }
- }
- catch
- {
- // Process ended.
- }
- }
-
- private void CleanupProcess(bool notify)
- {
- if (_process is null)
- return;
-
- _process.ErrorDataReceived -= OnErrorDataReceived;
- _process.Exited -= OnProcessExited;
- _process.Dispose();
- _process = null;
- ActiveServer = null;
-
- if (notify)
- RunningStateChanged?.Invoke(this, false);
- }
-
- private string GetRecentStderr()
- {
- lock (_stderrLock)
- {
- return _recentStderr.ToString().Trim();
- }
- }
-
private static string FormatStartupError(string stderr)
{
if (stderr.Contains("10808", StringComparison.Ordinal) ||
diff --git a/src/v2rayF.Desktop/Services/DesktopCoreEnvironment.cs b/src/v2rayF.Desktop/Services/DesktopCoreEnvironment.cs
index 916abc9..5e77aed 100644
--- a/src/v2rayF.Desktop/Services/DesktopCoreEnvironment.cs
+++ b/src/v2rayF.Desktop/Services/DesktopCoreEnvironment.cs
@@ -29,4 +29,6 @@ public string GetDataDirectory()
Directory.CreateDirectory(folder);
return folder;
}
+
+ public ICoreProcessHost CreateProcessHost() => new ManagedCoreProcessHost();
}
diff --git a/src/v2rayF/ViewModels/MainWindowViewModel.cs b/src/v2rayF/ViewModels/MainWindowViewModel.cs
index 1dc58c3..290dd1c 100644
--- a/src/v2rayF/ViewModels/MainWindowViewModel.cs
+++ b/src/v2rayF/ViewModels/MainWindowViewModel.cs
@@ -333,19 +333,19 @@ private async Task ConnectMobileAsync()
{
if (SelectedServer is null)
{
- StatusText = "Select a server first.";
+ RunOnUiThread(() => StatusText = "Select a server first.");
return;
}
if (!_proxyCore.IsCoreAvailable() || !_proxyCore.HasGeoFiles())
{
- await AppServices.CoreEnvironment.EnsureCoreAsync().ConfigureAwait(true);
- UpdateCoreStatus();
+ await AppServices.CoreEnvironment.EnsureCoreAsync().ConfigureAwait(false);
+ RunOnUiThread(UpdateCoreStatus);
}
if (!_proxyCore.IsCoreAvailable())
{
- StatusText = "Xray core not found.";
+ RunOnUiThread(() => StatusText = "Xray core not found.");
return;
}
@@ -355,29 +355,36 @@ private async Task ConnectMobileAsync()
if (settings.RoutingMode == RoutingMode.BypassChina && !_proxyCore.HasGeoFiles())
settings.RoutingMode = RoutingMode.BypassLan;
- await _settingsStore.SaveAsync(settings).ConfigureAwait(true);
+ await _settingsStore.SaveAsync(settings).ConfigureAwait(false);
var vpnEngaged = false;
try
{
- IsBusy = true;
- StatusText = $"Connecting to {SelectedServer.Name}…";
+ RunOnUiThread(() =>
+ {
+ IsBusy = true;
+ StatusText = $"Connecting to {SelectedServer.Name}…";
+ });
+
await ConnectAndroidAsync(SelectedServer, settings, engaged => vpnEngaged = engaged)
- .ConfigureAwait(true);
+ .ConfigureAwait(false);
}
catch (Exception ex)
{
- await SafeTeardownAsync(vpnEngaged).ConfigureAwait(true);
- StatusText = $"Connection failed: {ex.Message}";
+ await SafeTeardownAsync(vpnEngaged).ConfigureAwait(false);
+ RunOnUiThread(() => StatusText = $"Connection failed: {ex.Message}");
}
finally
{
if (!IsConnected && vpnEngaged)
- await SafeTeardownAsync(vpnEngaged: true).ConfigureAwait(true);
+ await SafeTeardownAsync(vpnEngaged: true).ConfigureAwait(false);
- IsBusy = false;
- OnPropertyChanged(nameof(ConnectButtonText));
- OnPropertyChanged(nameof(TrayToolTip));
+ RunOnUiThread(() =>
+ {
+ IsBusy = false;
+ OnPropertyChanged(nameof(ConnectButtonText));
+ OnPropertyChanged(nameof(TrayToolTip));
+ });
}
}
@@ -448,7 +455,8 @@ private async Task ConnectAndroidAsync(
AppSettings settings,
Action markVpnEngaged)
{
- StatusText = $"Checking {server.Name}…";
+ RunOnUiThread(() => StatusText = $"Checking {server.Name}…");
+
var probeSettings = new AppSettings
{
RoutingMode = settings.RoutingMode,
@@ -458,24 +466,28 @@ private async Task ConnectAndroidAsync(
SubscriptionUrl = settings.SubscriptionUrl
};
- await _proxyCore.StartAsync(server, probeSettings, null).ConfigureAwait(true);
- await _proxyCore.StopAsync().ConfigureAwait(true);
+ await _proxyCore.StartAsync(server, probeSettings, null).ConfigureAwait(false);
+ await _proxyCore.StopAsync().ConfigureAwait(false);
- StatusText = "Starting VPN…";
- var tunFd = await AppServices.Platform.EstablishVpnAsync().ConfigureAwait(true);
+ RunOnUiThread(() => StatusText = "Starting VPN…");
+ var tunFd = await AppServices.Platform.EstablishVpnAsync().ConfigureAwait(false);
if (tunFd is null)
{
- StatusText = GetAndroidVpnFailureMessage();
+ RunOnUiThread(() => StatusText = GetAndroidVpnFailureMessage());
return;
}
markVpnEngaged(true);
- StatusText = $"Starting proxy for {server.Name}…";
- await _proxyCore.StartAsync(server, settings, tunFd).ConfigureAwait(true);
- await AppServices.Platform.EnableProxyAsync().ConfigureAwait(true);
- StatusText = $"Connected — {server.Name} (VPN)";
- IsConnected = true;
+ RunOnUiThread(() => StatusText = $"Starting proxy for {server.Name}…");
+ await _proxyCore.StartAsync(server, settings, tunFd).ConfigureAwait(false);
+ await AppServices.Platform.EnableProxyAsync().ConfigureAwait(false);
+
+ RunOnUiThread(() =>
+ {
+ StatusText = $"Connected — {server.Name} (VPN)";
+ IsConnected = true;
+ });
}
private async Task SafeTeardownAsync(bool vpnEngaged)
@@ -501,7 +513,7 @@ private async Task SafeTeardownAsync(bool vpnEngaged)
}
}
- IsConnected = false;
+ RunOnUiThread(() => IsConnected = false);
}
private Task EmergencyDisconnectAsync() => SafeTeardownAsync(vpnEngaged: true);