diff --git a/src/App.Android/MainActivity.cs b/src/App.Android/MainActivity.cs index 0d0cd3c..2e0704b 100644 --- a/src/App.Android/MainActivity.cs +++ b/src/App.Android/MainActivity.cs @@ -4,6 +4,7 @@ using Android.Content; using Android.Content.PM; using Android.OS; using Avalonia.Android; +using AvaloniaApp = PalladiumWallet.App.App; namespace PalladiumWallet.Mobile; @@ -17,6 +18,7 @@ public class MainActivity : AvaloniaMainActivity internal const int ScanRequestCode = 9001; internal static TaskCompletionSource? ScanTcs; internal static MainActivity? Current; + private bool _wasPaused; protected override void OnCreate(Bundle? savedInstanceState) { @@ -24,6 +26,26 @@ public class MainActivity : AvaloniaMainActivity Current = this; } + protected override void OnPause() + { + base.OnPause(); + _wasPaused = true; + } + + protected override void OnResume() + { + base.OnResume(); + if (!_wasPaused) return; + _wasPaused = false; + // The TCP socket can die silently while the screen was off/locked (Doze, + // mobile radio suspend, NAT timeout) without the app ever observing the + // failure. Force an immediate health check instead of waiting for the next + // 20s keep-alive tick, which may itself have been suspended for longer than + // the lock. + if (AvaloniaApp.MainViewModel is { } vm) + _ = vm.CheckConnectionOnResumeAsync(); + } + protected override void OnActivityResult(int requestCode, Result resultCode, Intent? data) { base.OnActivityResult(requestCode, resultCode, data); diff --git a/src/App/App.axaml.cs b/src/App/App.axaml.cs index f76d859..e4f8635 100644 --- a/src/App/App.axaml.cs +++ b/src/App/App.axaml.cs @@ -8,6 +8,10 @@ namespace PalladiumWallet.App; public partial class App : Application { + /// Set once the single ViewModel is created; lets platform heads (e.g. the + /// Android activity) reach it for lifecycle events without a second instance. + public static MainWindowViewModel? MainViewModel { get; private set; } + public override void Initialize() { AvaloniaXamlLoader.Load(this); @@ -16,6 +20,7 @@ public partial class App : Application public override void OnFrameworkInitializationCompleted() { var vm = new MainWindowViewModel(); + MainViewModel = vm; // Desktop (Windows/Linux): classic window. Mobile (Android): single // view. Same shared UI (MainView) and same ViewModel. diff --git a/src/App/ViewModels/MainWindowViewModel.cs b/src/App/ViewModels/MainWindowViewModel.cs index b538981..a4a3cb4 100644 --- a/src/App/ViewModels/MainWindowViewModel.cs +++ b/src/App/ViewModels/MainWindowViewModel.cs @@ -165,7 +165,7 @@ public partial class MainWindowViewModel : ViewModelBase { if (IsSyncing) return; - if (_client is { IsConnected: true }) + if (_client is { IsConnected: true } client) { // If the wallet is open and the last sync failed, retry automatically. if (_syncFailed && _account is not null) @@ -173,8 +173,24 @@ public partial class MainWindowViewModel : ViewModelBase await ConnectAndSync(); return; } - try { await _client.PingAsync(); } - catch { } + try + { + await client.PingAsync(); + } + catch + { + // TcpClient.Connected only reflects the last known socket state, so a + // connection killed silently while the app was suspended (e.g. Android + // Doze after screen lock) still reports IsConnected == true. A failed + // ping is the only reliable signal here: tear the dead client down so + // the next tick reconnects instead of retrying forever on a dead socket. + await DisconnectAsync(); + if (_autoReconnect) + { + ConnectionStatus = Loc.Tr("conn.reconnecting"); + await ConnectAndSync(); + } + } } else if (_autoReconnect) { @@ -183,6 +199,17 @@ public partial class MainWindowViewModel : ViewModelBase } } + /// Forces an immediate connection health check, bypassing the 20s timer. + /// Called when the app resumes from background/lock screen, since the socket may + /// have died silently while suspended and the UI would otherwise show a stale + /// "connected" state until the next scheduled tick (or forever, if it never fires + /// during Doze). + public async System.Threading.Tasks.Task CheckConnectionOnResumeAsync() + { + if (IsSyncing) return; + await KeepAliveTickAsync(); + } + // ---- wallet lifecycle ---- [RelayCommand]