diff --git a/src/modules/MCPServer/MCPServer/Tools/AwakeTools.cs b/src/modules/MCPServer/MCPServer/Tools/AwakeTools.cs index b6bff69e4b..9f75253bef 100644 --- a/src/modules/MCPServer/MCPServer/Tools/AwakeTools.cs +++ b/src/modules/MCPServer/MCPServer/Tools/AwakeTools.cs @@ -2,7 +2,13 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.PowerToys.Settings.UI.Library; using ModelContextProtocol.Server; namespace PowerToys.MCPServer.Tools @@ -13,5 +19,103 @@ namespace PowerToys.MCPServer.Tools [McpServerTool] [Description("Echoes the message back to the client.")] public static string SetTimeTest(string message) => $"Hello {message}"; + + private sealed class AppUsageRecord + { + [JsonPropertyName("process")] + public string ProcessName { get; set; } = string.Empty; + + [JsonPropertyName("totalSeconds")] + public double TotalSeconds { get; set; } + + [JsonPropertyName("lastUpdatedUtc")] + public DateTime LastUpdatedUtc { get; set; } + + [JsonPropertyName("firstSeenUtc")] + public DateTime FirstSeenUtc { get; set; } + } + + [McpServerTool] + [Description("Get top N foreground app usage entries recorded by Awake (reads usage.json). Parameters: top (default 10), days (default 7). Returns JSON.")] + public static string GetAwakeUsageSummary(int top = 10, int days = 7) + { + try + { + SettingsUtils utils = new(); + string settingsPath = utils.GetSettingsFilePath("Awake"); + string directory = Path.GetDirectoryName(settingsPath)!; + string usageFile = Path.Combine(directory, "usage.json"); + + if (!File.Exists(usageFile)) + { + return JsonSerializer.Serialize(new { error = "usage.json not found", path = usageFile }); + } + + string json = File.ReadAllText(usageFile); + using JsonDocument doc = JsonDocument.Parse(json); + + DateTime cutoff = DateTime.UtcNow.AddDays(-Math.Max(1, days)); + + var result = doc.RootElement + .EnumerateArray() + .Select(e => new + { + process = e.GetPropertyOrDefault("process", string.Empty), + totalSeconds = Math.Round(e.GetPropertyOrDefault("totalSeconds", 0.0), 1), + lastUpdatedUtc = e.GetPropertyOrDefaultDateTime("lastUpdatedUtc"), + firstSeenUtc = e.GetPropertyOrDefaultDateTime("firstSeenUtc"), + }) + .Where(r => r.lastUpdatedUtc >= cutoff) + .OrderByDescending(r => r.totalSeconds) + .Take(top) + .Select(r => new + { + r.process, + r.totalSeconds, + totalHours = Math.Round(r.totalSeconds / 3600.0, 2), + r.firstSeenUtc, + r.lastUpdatedUtc, + }); + + return JsonSerializer.Serialize(result); + } + catch (Exception ex) + { + return JsonSerializer.Serialize(new { error = ex.Message }); + } + } + + private static string GetPropertyOrDefault(this JsonElement element, string name, string defaultValue) + { + if (element.ValueKind == JsonValueKind.Object && element.TryGetProperty(name, out JsonElement value) && value.ValueKind == JsonValueKind.String) + { + return value.GetString() ?? defaultValue; + } + + return defaultValue; + } + + private static double GetPropertyOrDefault(this JsonElement element, string name, double defaultValue) + { + if (element.ValueKind == JsonValueKind.Object && element.TryGetProperty(name, out JsonElement value) && value.ValueKind == JsonValueKind.Number && value.TryGetDouble(out double d)) + { + return d; + } + + return defaultValue; + } + + private static DateTime GetPropertyOrDefaultDateTime(this JsonElement element, string name) + { + if (element.ValueKind == JsonValueKind.Object && element.TryGetProperty(name, out JsonElement value)) + { + if (value.ValueKind == JsonValueKind.String && value.TryGetDateTime(out DateTime dt)) + { + return dt; + } + } + + return DateTime.MinValue; + } } } diff --git a/src/modules/awake/Awake/Core/Manager.cs b/src/modules/awake/Awake/Core/Manager.cs index df4ac87581..7a0a45d7bf 100644 --- a/src/modules/awake/Awake/Core/Manager.cs +++ b/src/modules/awake/Awake/Core/Manager.cs @@ -17,6 +17,9 @@ using System.Text.Json; using System.Threading; using Awake.Core.Models; using Awake.Core.Native; + +// New usage tracking namespace +using Awake.Core.Usage; using Awake.Properties; using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Library; @@ -56,6 +59,9 @@ namespace Awake.Core private static readonly BlockingCollection _stateQueue; private static CancellationTokenSource _tokenSource; + // Foreground usage tracker instance (lifecycle managed by Program) + internal static ForegroundUsageTracker? UsageTracker { get; set; } + static Manager() { _tokenSource = new CancellationTokenSource(); @@ -412,6 +418,16 @@ namespace Awake.Core Bridge.DestroyWindow(TrayHelper.WindowHandle); } + // Dispose usage tracker (flushes data) + try + { + UsageTracker?.Dispose(); + } + catch (Exception ex) + { + Logger.LogWarning($"Failed disposing UsageTracker: {ex.Message}"); + } + Bridge.PostQuitMessage(exitCode); Environment.Exit(exitCode); } diff --git a/src/modules/awake/Awake/Core/Native/IdleTime.cs b/src/modules/awake/Awake/Core/Native/IdleTime.cs new file mode 100644 index 0000000000..b1e237ae6f --- /dev/null +++ b/src/modules/awake/Awake/Core/Native/IdleTime.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; + +namespace Awake.Core.Native +{ + internal static class IdleTime + { + // Keep original native field names but suppress StyleCop (interop requires exact names). + [StructLayout(LayoutKind.Sequential)] + private struct LASTINPUTINFO + { +#pragma warning disable SA1307 // Interop field naming + public uint cbSize; + public uint dwTime; +#pragma warning restore SA1307 + } + + [DllImport("user32.dll")] + private static extern bool GetLastInputInfo(ref LASTINPUTINFO plii); + + public static TimeSpan GetIdleTime() + { + LASTINPUTINFO info = new() + { + cbSize = (uint)Marshal.SizeOf(), + }; + + if (!GetLastInputInfo(ref info)) + { + return TimeSpan.Zero; + } + + // Calculate elapsed milliseconds since last input considering Environment.TickCount wrap. + uint lastInputTicks = info.dwTime; + uint nowTicks = (uint)Environment.TickCount; + uint delta = nowTicks >= lastInputTicks ? nowTicks - lastInputTicks : (uint.MaxValue - lastInputTicks) + nowTicks + 1; + return TimeSpan.FromMilliseconds(delta); + } + } +} diff --git a/src/modules/awake/Awake/Core/Usage/ForegroundUsageTracker.cs b/src/modules/awake/Awake/Core/Usage/ForegroundUsageTracker.cs new file mode 100644 index 0000000000..ea86b96b7e --- /dev/null +++ b/src/modules/awake/Awake/Core/Usage/ForegroundUsageTracker.cs @@ -0,0 +1,459 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text.Json; +using System.Timers; +using Awake.Core.Native; +using Awake.Core.Usage.Models; +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library; + +namespace Awake.Core.Usage +{ + /// + /// Tracks foreground application usage time (simple active window focus durations) with idle suppression. + /// + internal sealed class ForegroundUsageTracker : IDisposable + { + private const uint EventSystemForeground = 0x0003; + private const uint WinEventOutOfContext = 0x0000; + + private static readonly JsonSerializerOptions SerializerOptions = new() { WriteIndented = true }; + + private delegate void WinEventDelegate( + IntPtr hWinEventHook, + uint eventType, + IntPtr hwnd, + int idObject, + int idChild, + uint dwEventThread, + uint dwmsEventTime); + + [DllImport("user32.dll")] + private static extern IntPtr SetWinEventHook( + uint eventMin, + uint eventMax, + IntPtr hmodWinEventProc, + WinEventDelegate lpfnWinEventProc, + uint idProcess, + uint idThread, + uint dwFlags); + + [DllImport("user32.dll")] + private static extern bool UnhookWinEvent(IntPtr hWinEventHook); + + [DllImport("user32.dll")] + private static extern IntPtr GetForegroundWindow(); + + [DllImport("user32.dll")] + private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId); + + private readonly object _lock = new(); + private readonly Dictionary _usage = new(StringComparer.OrdinalIgnoreCase); + private readonly string _storePath; + private readonly Timer _flushTimer; + private readonly TimeSpan _idleThreshold = TimeSpan.FromSeconds(60); + private readonly Timer _pollTimer; // Fallback polling when WinEvent hook not firing + + private string? _activeProcess; + private DateTime _activeSince; + private bool _disposed; + private IntPtr _hook = IntPtr.Zero; + private WinEventDelegate? _hookDelegate; + private int _retentionDays; + private IntPtr _lastHwnd = IntPtr.Zero; + + private const double CommitThresholdSeconds = 0.25; + + internal bool Enabled { get; private set; } + + public ForegroundUsageTracker(string storePath, int retentionDays) + { + _storePath = storePath; + _retentionDays = retentionDays; + Directory.CreateDirectory(Path.GetDirectoryName(storePath)!); + + LoadState(); + + _flushTimer = new Timer(30000) + { + AutoReset = true, + }; + _flushTimer.Elapsed += (_, _) => FlushInternal(); + + _pollTimer = new Timer(1000) + { + AutoReset = true, + }; + _pollTimer.Elapsed += (_, _) => PollForeground(); + } + + public void Configure(bool enabled, int retentionDays) + { + _retentionDays = Math.Max(1, retentionDays); + if (enabled == Enabled) + { + return; + } + + Enabled = enabled; + + if (Enabled) + { + _activeSince = DateTime.UtcNow; + _hookDelegate = WinEventCallback; + _hook = SetWinEventHook(EventSystemForeground, EventSystemForeground, IntPtr.Zero, _hookDelegate, 0, 0, WinEventOutOfContext); + Logger.LogInfo(_hook != IntPtr.Zero ? "[AwakeUsage] WinEvent hook installed." : "[AwakeUsage] WinEvent hook failed (fallback polling only)."); + CaptureInitialForeground(); + _flushTimer.Start(); + _pollTimer.Start(); + Logger.LogInfo("[AwakeUsage] Started foreground usage tracking."); + } + else + { + _flushTimer.Stop(); + _pollTimer.Stop(); + if (_hook != IntPtr.Zero) + { + UnhookWinEvent(_hook); + _hook = IntPtr.Zero; + } + + CommitActiveSpan(); + FlushInternal(); + Logger.LogInfo("[AwakeUsage] Stopped foreground usage tracking."); + } + } + + private void CaptureInitialForeground() + { + try + { + IntPtr hwnd = GetForegroundWindow(); + if (hwnd == IntPtr.Zero) + { + Logger.LogDebug("[AwakeUsage] Initial foreground hwnd == 0."); + return; + } + + if (TryResolveProcess(hwnd, out string? procName)) + { + _activeProcess = procName; + _activeSince = DateTime.UtcNow; + _lastHwnd = hwnd; + EnsurePlaceholder(_activeProcess); + Logger.LogInfo("[AwakeUsage] Initial foreground captured: " + _activeProcess); + } + } + catch (Exception ex) + { + Logger.LogWarning("[AwakeUsage] Failed to capture initial foreground: " + ex.Message); + } + } + + private static string SafeResolveProcessName(Process proc) + { + try + { + return Path.GetFileName(proc.MainModule?.FileName) ?? proc.ProcessName; + } + catch + { + return proc.ProcessName; + } + } + + private bool TryResolveProcess(IntPtr hwnd, out string? processName) + { + processName = null; + + if (hwnd == IntPtr.Zero) + { + return false; + } + + try + { + uint pid; + uint tid = GetWindowThreadProcessId(hwnd, out pid); + if (tid == 0 || pid == 0) + { + return false; + } + + using Process p = Process.GetProcessById((int)pid); + processName = SafeResolveProcessName(p); + return !string.IsNullOrWhiteSpace(processName); + } + catch (Exception ex) + { + Logger.LogDebug("[AwakeUsage] TryResolveProcess failed: " + ex.Message); + return false; + } + } + + private void EnsurePlaceholder(string? process) + { + if (string.IsNullOrWhiteSpace(process)) + { + return; + } + + lock (_lock) + { + if (!_usage.ContainsKey(process)) + { + _usage[process] = new AppUsageRecord + { + ProcessName = process, + FirstSeenUtc = DateTime.UtcNow, + LastUpdatedUtc = DateTime.UtcNow, + TotalSeconds = 0, + }; + } + } + } + + private void WinEventCallback(IntPtr hWinEventHook, uint evt, IntPtr hwnd, int idObj, int idChild, uint thread, uint time) + { + if (_disposed || !Enabled || evt != EventSystemForeground) + { + return; + } + + Logger.LogDebug($"[AwakeUsage] WinEvent foreground change: hwnd=0x{hwnd.ToInt64():X}"); + HandleForegroundChange(hwnd, source: "hook"); + } + + private void PollForeground() + { + if (_disposed || !Enabled) + { + return; + } + + IntPtr hwnd = GetForegroundWindow(); + if (hwnd == IntPtr.Zero || hwnd == _lastHwnd) + { + return; + } + + Logger.LogDebug($"[AwakeUsage] Poll detected change: hwnd=0x{hwnd.ToInt64():X}"); + HandleForegroundChange(hwnd, source: "poll"); + } + + private void HandleForegroundChange(IntPtr hwnd, string source) + { + try + { + CommitActiveSpan(); + if (!TryResolveProcess(hwnd, out string? procName)) + { + _activeProcess = null; + return; + } + + _activeProcess = procName; + _activeSince = DateTime.UtcNow; + _lastHwnd = hwnd; + EnsurePlaceholder(_activeProcess); + Logger.LogDebug($"[AwakeUsage] Active process set ({source}): {_activeProcess}"); + } + catch (Exception ex) + { + Logger.LogWarning("[AwakeUsage] HandleForegroundChange failed: " + ex.Message); + } + } + + private void CommitActiveSpan() + { + if (string.IsNullOrEmpty(_activeProcess)) + { + return; + } + + if (IdleTime.GetIdleTime() > _idleThreshold) + { + _activeProcess = null; + return; + } + + double seconds = (DateTime.UtcNow - _activeSince).TotalSeconds; + if (seconds < CommitThresholdSeconds) + { + return; + } + + lock (_lock) + { + if (!_usage.TryGetValue(_activeProcess!, out AppUsageRecord? rec)) + { + rec = new AppUsageRecord + { + ProcessName = _activeProcess!, + FirstSeenUtc = DateTime.UtcNow, + LastUpdatedUtc = DateTime.UtcNow, + TotalSeconds = 0, + }; + _usage[_activeProcess!] = rec; + } + + rec.TotalSeconds += seconds; + rec.LastUpdatedUtc = DateTime.UtcNow; + } + + _activeSince = DateTime.UtcNow; + } + + private void LoadState() + { + try + { + if (!File.Exists(_storePath)) + { + return; + } + + string json = File.ReadAllText(_storePath); + List? list = JsonSerializer.Deserialize>(json); + if (list == null) + { + return; + } + + DateTime cutoff = DateTime.UtcNow.AddDays(-_retentionDays); + foreach (AppUsageRecord rec in list.Where(r => r.LastUpdatedUtc >= cutoff)) + { + _usage[rec.ProcessName] = rec; + } + } + catch (Exception ex) + { + Logger.LogWarning("[AwakeUsage] Failed to load usage store: " + ex.Message); + } + } + + private double GetLiveActiveSeconds() + { + if (string.IsNullOrEmpty(_activeProcess)) + { + return 0; + } + + if (IdleTime.GetIdleTime() > _idleThreshold) + { + return 0; + } + + return Math.Max(0, (DateTime.UtcNow - _activeSince).TotalSeconds); + } + + private void FlushInternal() + { + try + { + CommitActiveSpan(); + + List snapshot; + double liveSeconds = GetLiveActiveSeconds(); + string? liveProcess = _activeProcess; + lock (_lock) + { + DateTime cutoff = DateTime.UtcNow.AddDays(-_retentionDays); + foreach (string key in _usage.Values.Where(v => v.LastUpdatedUtc < cutoff).Select(v => v.ProcessName).ToList()) + { + _usage.Remove(key); + } + + snapshot = _usage.Values + .Select(r => new AppUsageRecord + { + ProcessName = r.ProcessName, + TotalSeconds = r.ProcessName.Equals(liveProcess, StringComparison.OrdinalIgnoreCase) ? r.TotalSeconds + liveSeconds : r.TotalSeconds, + FirstSeenUtc = r.FirstSeenUtc, + LastUpdatedUtc = r.LastUpdatedUtc, + }) + .OrderByDescending(r => r.TotalSeconds) + .ToList(); + + if (liveProcess != null && !_usage.ContainsKey(liveProcess) && liveSeconds > 0) + { + snapshot.Add(new AppUsageRecord + { + ProcessName = liveProcess, + TotalSeconds = liveSeconds, + FirstSeenUtc = DateTime.UtcNow, + LastUpdatedUtc = DateTime.UtcNow, + }); + snapshot = snapshot.OrderByDescending(r => r.TotalSeconds).ToList(); + } + } + + string json = JsonSerializer.Serialize(snapshot, SerializerOptions); + File.WriteAllText(_storePath, json); + } + catch (Exception ex) + { + Logger.LogWarning("[AwakeUsage] Flush failed: " + ex.Message); + } + } + + public IReadOnlyList GetSummary(int top, int days) + { + CommitActiveSpan(); + double liveSeconds = GetLiveActiveSeconds(); + string? liveProcess = _activeProcess; + + lock (_lock) + { + DateTime cutoff = DateTime.UtcNow.AddDays(-Math.Max(1, days)); + List list = _usage.Values + .Where(r => r.LastUpdatedUtc >= cutoff) + .Select(r => new AppUsageRecord + { + ProcessName = r.ProcessName, + TotalSeconds = r.ProcessName.Equals(liveProcess, StringComparison.OrdinalIgnoreCase) ? r.TotalSeconds + liveSeconds : r.TotalSeconds, + FirstSeenUtc = r.FirstSeenUtc, + LastUpdatedUtc = r.LastUpdatedUtc, + }) + .ToList(); + + if (liveProcess != null && list.All(r => !r.ProcessName.Equals(liveProcess, StringComparison.OrdinalIgnoreCase)) && liveSeconds > 0) + { + list.Add(new AppUsageRecord + { + ProcessName = liveProcess, + TotalSeconds = liveSeconds, + FirstSeenUtc = DateTime.UtcNow, + LastUpdatedUtc = DateTime.UtcNow, + }); + } + + return list + .OrderByDescending(r => r.TotalSeconds) + .Take(top) + .ToList(); + } + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + Configure(false, _retentionDays); + _flushTimer.Dispose(); + _pollTimer.Dispose(); + } + } +} diff --git a/src/modules/awake/Awake/Core/Usage/Models/AppUsageRecord.cs b/src/modules/awake/Awake/Core/Usage/Models/AppUsageRecord.cs new file mode 100644 index 0000000000..83c0035231 --- /dev/null +++ b/src/modules/awake/Awake/Core/Usage/Models/AppUsageRecord.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Text.Json.Serialization; + +namespace Awake.Core.Usage.Models +{ + internal sealed class AppUsageRecord + { + [JsonPropertyName("process")] + public string ProcessName { get; set; } = string.Empty; + + [JsonPropertyName("totalSeconds")] + public double TotalSeconds { get; set; } + + [JsonPropertyName("lastUpdatedUtc")] + public DateTime LastUpdatedUtc { get; set; } + + [JsonPropertyName("firstSeenUtc")] + public DateTime FirstSeenUtc { get; set; } + } +} diff --git a/src/modules/awake/Awake/Program.cs b/src/modules/awake/Awake/Program.cs index 23882b3018..33dc2eafbd 100644 --- a/src/modules/awake/Awake/Program.cs +++ b/src/modules/awake/Awake/Program.cs @@ -18,6 +18,9 @@ using System.Threading.Tasks; using Awake.Core; using Awake.Core.Models; using Awake.Core.Native; + +// Usage tracking +using Awake.Core.Usage; using Awake.Properties; using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Library; @@ -342,6 +345,33 @@ namespace Awake } } } + + // Initialize usage tracking + InitializeUsageTracking(); + } + + private static void InitializeUsageTracking() + { + try + { + string settingsPath = _settingsUtils!.GetSettingsFilePath(Core.Constants.AppName); + string directory = Path.GetDirectoryName(settingsPath)!; + string usageFile = Path.Combine(directory, "usage.json"); + + AwakeSettings settings = _settingsUtils.GetSettings(Core.Constants.AppName) ?? new AwakeSettings(); + + if (Manager.UsageTracker == null) + { + Manager.UsageTracker = new ForegroundUsageTracker(usageFile, settings.Properties.UsageRetentionDays); + } + + Manager.UsageTracker.Configure(settings.Properties.TrackUsageEnabled, settings.Properties.UsageRetentionDays); + Logger.LogInfo($"Usage tracking configured (enabled={settings.Properties.TrackUsageEnabled}, retentionDays={settings.Properties.UsageRetentionDays})."); + } + catch (Exception ex) + { + Logger.LogWarning($"Failed to initialize usage tracking: {ex.Message}"); + } } private static void AllocateLocalConsole() @@ -361,6 +391,7 @@ namespace Awake SetupFileSystemWatcher(settingsPath); InitializeSettings(); ProcessSettings(); + InitializeUsageTracking(); // after initial settings load } catch (Exception ex) { @@ -406,6 +437,7 @@ namespace Awake { Logger.LogInfo("Detected a settings file change. Updating configuration..."); ProcessSettings(); + InitializeUsageTracking(); // re-evaluate usage tracking on config change } catch (Exception e) { diff --git a/src/settings-ui/Settings.UI.Library/AwakeProperties.cs b/src/settings-ui/Settings.UI.Library/AwakeProperties.cs index 7fd08be476..61f2787ed6 100644 --- a/src/settings-ui/Settings.UI.Library/AwakeProperties.cs +++ b/src/settings-ui/Settings.UI.Library/AwakeProperties.cs @@ -20,6 +20,11 @@ namespace Microsoft.PowerToys.Settings.UI.Library IntervalMinutes = 1; ExpirationDateTime = DateTimeOffset.Now; CustomTrayTimes = []; + + // New usage tracking defaults (opt-in, disabled by default) + // Need to add this to settings, set it to true for testing + TrackUsageEnabled = true; + UsageRetentionDays = 14; // two weeks default retention } [JsonPropertyName("keepDisplayOn")] @@ -40,5 +45,13 @@ namespace Microsoft.PowerToys.Settings.UI.Library [JsonPropertyName("customTrayTimes")] [CmdConfigureIgnore] public Dictionary CustomTrayTimes { get; set; } + + // New opt-in usage tracking flag + [JsonPropertyName("trackUsageEnabled")] + public bool TrackUsageEnabled { get; set; } + + // Retention window for usage data (days) + [JsonPropertyName("usageRetentionDays")] + public int UsageRetentionDays { get; set; } } } diff --git a/src/settings-ui/Settings.UI.Library/AwakeSettings.cs b/src/settings-ui/Settings.UI.Library/AwakeSettings.cs index fc1b0d1a0f..c35c734e78 100644 --- a/src/settings-ui/Settings.UI.Library/AwakeSettings.cs +++ b/src/settings-ui/Settings.UI.Library/AwakeSettings.cs @@ -38,9 +38,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library KeepDisplayOn = Properties.KeepDisplayOn, IntervalMinutes = Properties.IntervalMinutes, IntervalHours = Properties.IntervalHours, - - // Fix old buggy default value that might be saved in Settings. Some components don't deal well with negative time zones and minimum time offsets. ExpirationDateTime = Properties.ExpirationDateTime.Year < 2 ? DateTimeOffset.Now : Properties.ExpirationDateTime, + TrackUsageEnabled = Properties.TrackUsageEnabled, + UsageRetentionDays = Properties.UsageRetentionDays, }, }; }