/* * Copyright (c) 2025 Proton AG * * This file is part of ProtonVPN. * * ProtonVPN is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * ProtonVPN is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with ProtonVPN. If not, see . */ using ProtonVPN.Client.Common.Observers; using ProtonVPN.Client.EventMessaging.Contracts; using ProtonVPN.Client.Logic.Auth.Contracts.Messages; using ProtonVPN.Client.Settings.Contracts; using ProtonVPN.IssueReporting.Contracts; using ProtonVPN.Logging.Contracts; using ProtonVPN.Logging.Contracts.Events.AppLogs; using ProtonVPN.StatisticalEvents.Contracts; using ProtonVPN.Common.Core.Extensions; namespace ProtonVPN.Client.Services; public class SettingsHeartbeatService : PollingObserverBase, IEventMessageReceiver, IEventMessageReceiver { private readonly static TimeSpan HEARTBEAT_MINIMUM_DELAY = TimeSpan.FromMinutes(3); private readonly static TimeSpan HEARTBEAT_INTERVAL = TimeSpan.FromHours(24); private readonly ISettingsHeartbeatReporter _settingsHeartbeatReporter; private readonly ISettings _settings; protected override TimeSpan PollingInterval { get; } = HEARTBEAT_INTERVAL; protected override TimeSpan? InitialDelay => CalculateInitialDelay(); public SettingsHeartbeatService( ISettingsHeartbeatReporter settingsHeartbeatReporter, ILogger logger, IIssueReporter issueReporter, ISettings settings) : base(logger, issueReporter) { _settingsHeartbeatReporter = settingsHeartbeatReporter; _settings = settings; } public void Receive(LoggedInMessage message) { try { if (_settings.LastSettingsHeartbeatTimeUtc == null) { _settings.LastSettingsHeartbeatTimeUtc = DateTimeOffset.UtcNow; } Start(); } catch (Exception ex) { Logger.Error("Failed to start settings heartbeat service", ex); } } public void Receive(LoggedOutMessage message) { try { Stop(); } catch (Exception ex) { Logger.Error("Failed to stop settings heartbeat service", ex); } } public void Start() { Logger.Debug($"Settings heartbeat scheduled in: {InitialDelay ?? TimeSpan.Zero}"); StartTimer(); } public void Stop() { StopTimer(); } protected override async Task OnTriggerAsync() { try { await _settingsHeartbeatReporter.ReportAsync(); _settings.LastSettingsHeartbeatTimeUtc = DateTimeOffset.UtcNow; Logger.Debug($"Settings heartbeat sent at {_settings.LastSettingsHeartbeatTimeUtc}"); } catch (Exception ex) { Logger.Warn("Failed to send settings heartbeat", ex); } } private TimeSpan CalculateInitialDelay() { TimeSpan delay = TimeSpan.Zero; if (_settings.LastSettingsHeartbeatTimeUtc != null) { DateTimeOffset nextHeartbeatTime = _settings.LastSettingsHeartbeatTimeUtc.Value.Add(PollingInterval); delay = nextHeartbeatTime - DateTimeOffset.UtcNow; } return delay.Clamp(HEARTBEAT_MINIMUM_DELAY, HEARTBEAT_INTERVAL); } }