diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 94fb0a8903..95106bd1de 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -227,6 +227,7 @@ clientside CLIPCHILDREN Clipperton CLIPSIBLINGS +Cloneable clrcall Cls CLSCTX diff --git a/src/modules/awake/Awake/Core/APIHelper.cs b/src/modules/awake/Awake/Core/APIHelper.cs index 91aff0e629..f0f303db1e 100644 --- a/src/modules/awake/Awake/Core/APIHelper.cs +++ b/src/modules/awake/Awake/Core/APIHelper.cs @@ -6,6 +6,8 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Reactive.Concurrency; +using System.Reactive.Linq; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -82,31 +84,54 @@ namespace Awake.Core } } - public static void SetIndefiniteKeepAwake(Action callback, Action failureCallback, bool keepDisplayOn = false) + private static bool SetAwakeStateBasedOnDisplaySetting(bool keepDisplayOn) { - PowerToysTelemetry.Log.WriteEvent(new Awake.Telemetry.AwakeIndefinitelyKeepAwakeEvent()); + if (keepDisplayOn) + { + return SetAwakeState(EXECUTION_STATE.ES_SYSTEM_REQUIRED | EXECUTION_STATE.ES_DISPLAY_REQUIRED | EXECUTION_STATE.ES_CONTINUOUS); + } + else + { + return SetAwakeState(EXECUTION_STATE.ES_SYSTEM_REQUIRED | EXECUTION_STATE.ES_CONTINUOUS); + } + } + public static void CancelExistingThread() + { _tokenSource.Cancel(); try { + _log.Info("Attempting to ensure that the thread is properly cleaned up..."); + if (_runnerThread != null && !_runnerThread.IsCanceled) { _runnerThread.Wait(_threadToken); } + + _log.Info("Thread is clean."); } catch (OperationCanceledException) { - _log.Info("Confirmed background thread cancellation when setting indefinite keep awake."); + _log.Info("Confirmed background thread cancellation when disabling explicit keep awake."); } _tokenSource = new CancellationTokenSource(); _threadToken = _tokenSource.Token; + _log.Info("Instantiating of new token source and thread token completed."); + } + + public static void SetIndefiniteKeepAwake(Action callback, Action failureCallback, bool keepDisplayOn = false) + { + PowerToysTelemetry.Log.WriteEvent(new Awake.Telemetry.AwakeIndefinitelyKeepAwakeEvent()); + + CancelExistingThread(); + try { - _runnerThread = Task.Run(() => RunIndefiniteLoop(keepDisplayOn), _threadToken) - .ContinueWith((result) => callback(result.Result), TaskContinuationOptions.OnlyOnRanToCompletion) + _runnerThread = Task.Run(() => RunIndefiniteJob(keepDisplayOn), _threadToken) + .ContinueWith((result) => callback, TaskContinuationOptions.OnlyOnRanToCompletion) .ContinueWith((result) => failureCallback, TaskContinuationOptions.NotOnRanToCompletion); } catch (Exception ex) @@ -117,80 +142,101 @@ namespace Awake.Core public static void SetNoKeepAwake() { - _tokenSource.Cancel(); + PowerToysTelemetry.Log.WriteEvent(new Awake.Telemetry.AwakeNoKeepAwakeEvent()); - try - { - if (_runnerThread != null && !_runnerThread.IsCanceled) - { - _runnerThread.Wait(_threadToken); - } - } - catch (OperationCanceledException) - { - _log.Info("Confirmed background thread cancellation when disabling explicit keep awake."); - } + CancelExistingThread(); } - public static void SetTimedKeepAwake(uint seconds, Action callback, Action failureCallback, bool keepDisplayOn = true) + public static void SetExpirableKeepAwake(DateTimeOffset expireAt, Action callback, Action failureCallback, bool keepDisplayOn = true) { - PowerToysTelemetry.Log.WriteEvent(new Awake.Telemetry.AwakeTimedKeepAwakeEvent()); + PowerToysTelemetry.Log.WriteEvent(new Awake.Telemetry.AwakeExpirableKeepAwakeEvent()); - _tokenSource.Cancel(); + CancelExistingThread(); - try + if (expireAt > DateTime.Now && expireAt != null) { - if (_runnerThread != null && !_runnerThread.IsCanceled) - { - _runnerThread.Wait(_threadToken); - } - } - catch (OperationCanceledException) - { - _log.Info("Confirmed background thread cancellation when setting timed keep awake."); - } - - _tokenSource = new CancellationTokenSource(); - _threadToken = _tokenSource.Token; - - _runnerThread = Task.Run(() => RunTimedLoop(seconds, keepDisplayOn), _threadToken) - .ContinueWith((result) => callback(result.Result), TaskContinuationOptions.OnlyOnRanToCompletion) - .ContinueWith((result) => failureCallback, TaskContinuationOptions.NotOnRanToCompletion); - } - - private static bool RunIndefiniteLoop(bool keepDisplayOn = false) - { - bool success; - if (keepDisplayOn) - { - success = SetAwakeState(EXECUTION_STATE.ES_SYSTEM_REQUIRED | EXECUTION_STATE.ES_DISPLAY_REQUIRED | EXECUTION_STATE.ES_CONTINUOUS); + _runnerThread = Task.Run(() => RunExpiringJob(expireAt, keepDisplayOn), _threadToken) + .ContinueWith((result) => callback, TaskContinuationOptions.OnlyOnRanToCompletion) + .ContinueWith((result) => failureCallback, TaskContinuationOptions.NotOnRanToCompletion); } else { - success = SetAwakeState(EXECUTION_STATE.ES_SYSTEM_REQUIRED | EXECUTION_STATE.ES_CONTINUOUS); + // The target date is not in the future. + _log.Error("The specified target date and time is not in the future."); + _log.Error($"Current time: {DateTime.Now}\tTarget time: {expireAt}"); } + } + + public static void SetTimedKeepAwake(uint seconds, Action callback, Action failureCallback, bool keepDisplayOn = true) + { + PowerToysTelemetry.Log.WriteEvent(new Awake.Telemetry.AwakeTimedKeepAwakeEvent()); + + CancelExistingThread(); + + _runnerThread = Task.Run(() => RunTimedJob(seconds, keepDisplayOn), _threadToken) + .ContinueWith((result) => callback, TaskContinuationOptions.OnlyOnRanToCompletion) + .ContinueWith((result) => failureCallback, TaskContinuationOptions.NotOnRanToCompletion); + } + + private static void RunExpiringJob(DateTimeOffset expireAt, bool keepDisplayOn = false) + { + bool success = false; + + // In case cancellation was already requested. + _threadToken.ThrowIfCancellationRequested(); try { + success = SetAwakeStateBasedOnDisplaySetting(keepDisplayOn); + if (success) { - _log.Info($"Initiated indefinite keep awake in background thread: {PInvoke.GetCurrentThreadId()}. Screen on: {keepDisplayOn}"); + _log.Info($"Initiated expirable keep awake in background thread: {PInvoke.GetCurrentThreadId()}. Screen on: {keepDisplayOn}"); - WaitHandle.WaitAny(new[] { _threadToken.WaitHandle }); - - return success; + Observable.Timer(expireAt, Scheduler.CurrentThread).Subscribe( + _ => + { + _log.Info($"Completed expirable thread in {PInvoke.GetCurrentThreadId()}."); + CancelExistingThread(); + }, + _tokenSource.Token); } else { - _log.Info("Could not successfully set up indefinite keep awake."); - return success; + _log.Info("Could not successfully set up expirable keep awake."); + } + } + catch (OperationCanceledException ex) + { + // Task was clearly cancelled. + _log.Info($"Background thread termination: {PInvoke.GetCurrentThreadId()}. Message: {ex.Message}"); + } + } + + private static void RunIndefiniteJob(bool keepDisplayOn = false) + { + // In case cancellation was already requested. + _threadToken.ThrowIfCancellationRequested(); + + try + { + bool success = SetAwakeStateBasedOnDisplaySetting(keepDisplayOn); + + if (success) + { + _log.Info($"Initiated indefinite keep awake in background thread: {PInvoke.GetCurrentThreadId()}. Screen on: {keepDisplayOn}"); + + WaitHandle.WaitAny(new[] { _threadToken.WaitHandle }); + } + else + { + _log.Info("Could not successfully set up indefinite keep awake."); } } catch (OperationCanceledException ex) { // Task was clearly cancelled. _log.Info($"Background thread termination: {PInvoke.GetCurrentThreadId()}. Message: {ex.Message}"); - return success; } } @@ -221,59 +267,38 @@ namespace Awake.Core } } - private static bool RunTimedLoop(uint seconds, bool keepDisplayOn = true) + private static void RunTimedJob(uint seconds, bool keepDisplayOn = true) { bool success = false; // In case cancellation was already requested. _threadToken.ThrowIfCancellationRequested(); + try { - if (keepDisplayOn) - { - success = SetAwakeState(EXECUTION_STATE.ES_SYSTEM_REQUIRED | EXECUTION_STATE.ES_DISPLAY_REQUIRED | EXECUTION_STATE.ES_CONTINUOUS); - } - else - { - success = SetAwakeState(EXECUTION_STATE.ES_SYSTEM_REQUIRED | EXECUTION_STATE.ES_CONTINUOUS); - } + success = SetAwakeStateBasedOnDisplaySetting(keepDisplayOn); if (success) { - _log.Info($"Initiated temporary keep awake in background thread: {PInvoke.GetCurrentThreadId()}. Screen on: {keepDisplayOn}"); + _log.Info($"Initiated timed keep awake in background thread: {PInvoke.GetCurrentThreadId()}. Screen on: {keepDisplayOn}"); - _timedLoopTimer = new System.Timers.Timer((seconds * 1000) + 1); - _timedLoopTimer.Elapsed += (s, e) => - { - _tokenSource.Cancel(); - - _timedLoopTimer.Stop(); - }; - - _timedLoopTimer.Disposed += (s, e) => - { - _log.Info("Old timer disposed."); - }; - - _timedLoopTimer.Start(); - - WaitHandle.WaitAny(new[] { _threadToken.WaitHandle }); - _timedLoopTimer.Stop(); - _timedLoopTimer.Dispose(); - - return success; + Observable.Timer(TimeSpan.FromSeconds(seconds), Scheduler.CurrentThread).Subscribe( + _ => + { + _log.Info($"Completed timed thread in {PInvoke.GetCurrentThreadId()}."); + CancelExistingThread(); + }, + _tokenSource.Token); } else { _log.Info("Could not set up timed keep-awake with display on."); - return success; } } catch (OperationCanceledException ex) { // Task was clearly cancelled. _log.Info($"Background thread termination: {PInvoke.GetCurrentThreadId()}. Message: {ex.Message}"); - return success; } } @@ -357,10 +382,12 @@ namespace Awake.Core public static Dictionary GetDefaultTrayOptions() { - Dictionary optionsList = new Dictionary(); - optionsList.Add("30 minutes", 1800); - optionsList.Add("1 hour", 3600); - optionsList.Add("2 hours", 7200); + Dictionary optionsList = new Dictionary + { + { "30 minutes", 1800 }, + { "1 hour", 3600 }, + { "2 hours", 7200 }, + }; return optionsList; } } diff --git a/src/modules/awake/Awake/Core/Models/BatteryReportingScale.cs b/src/modules/awake/Awake/Core/Models/BatteryReportingScale.cs deleted file mode 100644 index 1520662e47..0000000000 --- a/src/modules/awake/Awake/Core/Models/BatteryReportingScale.cs +++ /dev/null @@ -1,12 +0,0 @@ -// 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. - -namespace Awake.Core.Models -{ - public struct BatteryReportingScale - { - public uint Granularity; - public uint Capacity; - } -} diff --git a/src/modules/awake/Awake/Core/Models/ControlType.cs b/src/modules/awake/Awake/Core/Models/ControlType.cs deleted file mode 100644 index c7b37894cc..0000000000 --- a/src/modules/awake/Awake/Core/Models/ControlType.cs +++ /dev/null @@ -1,16 +0,0 @@ -// 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. - -namespace Awake.Core.Models -{ - // See: https://learn.microsoft.com/windows/console/handlerroutine - public enum ControlType - { - CTRL_C_EVENT = 0, - CTRL_BREAK_EVENT = 1, - CTRL_CLOSE_EVENT = 2, - CTRL_LOGOFF_EVENT = 5, - CTRL_SHUTDOWN_EVENT = 6, - } -} diff --git a/src/modules/awake/Awake/Core/Models/TrayCommands.cs b/src/modules/awake/Awake/Core/Models/TrayCommands.cs index cca274fd5f..041db7834e 100644 --- a/src/modules/awake/Awake/Core/Models/TrayCommands.cs +++ b/src/modules/awake/Awake/Core/Models/TrayCommands.cs @@ -11,7 +11,8 @@ namespace Awake.Core.Models TC_DISPLAY_SETTING = PInvoke.WM_USER + 1, TC_MODE_PASSIVE = PInvoke.WM_USER + 2, TC_MODE_INDEFINITE = PInvoke.WM_USER + 3, - TC_EXIT = PInvoke.WM_USER + 4, - TC_TIME = PInvoke.WM_USER + 5, + TC_MODE_EXPIRABLE = PInvoke.WM_USER + 4, + TC_EXIT = PInvoke.WM_USER + 100, + TC_TIME = PInvoke.WM_USER + 101, } } diff --git a/src/modules/awake/Awake/Core/TrayHelper.cs b/src/modules/awake/Awake/Core/TrayHelper.cs index a0f908ce2a..25f08b7855 100644 --- a/src/modules/awake/Awake/Core/TrayHelper.cs +++ b/src/modules/awake/Awake/Core/TrayHelper.cs @@ -20,6 +20,13 @@ using Windows.Win32.UI.WindowsAndMessaging; namespace Awake.Core { + /// + /// Helper class used to manage the system tray. + /// + /// + /// Because Awake is a console application, there is no built-in + /// way to embed UI components so we have to heavily rely on the native Windows API. + /// internal static class TrayHelper { private static readonly Logger _log; @@ -89,7 +96,7 @@ namespace Awake.Core text, settings.Properties.KeepDisplayOn, settings.Properties.Mode, - settings.Properties.TrayTimeShortcuts, + settings.Properties.CustomTrayTimes, startedFromPowerToys); } @@ -116,19 +123,18 @@ namespace Awake.Core trayTimeShortcuts.AddRange(APIHelper.GetDefaultTrayOptions()); } - // TODO: Make sure that this loads from JSON instead of being hard-coded. var awakeTimeMenu = new DestroyMenuSafeHandle(PInvoke.CreatePopupMenu(), false); for (int i = 0; i < trayTimeShortcuts.Count; i++) { PInvoke.InsertMenu(awakeTimeMenu, (uint)i, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING, (uint)TrayCommands.TC_TIME + (uint)i, trayTimeShortcuts.ElementAt(i).Key); } - var modeMenu = new DestroyMenuSafeHandle(PInvoke.CreatePopupMenu(), false); - PInvoke.InsertMenu(modeMenu, 0, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING | (mode == AwakeMode.PASSIVE ? MENU_ITEM_FLAGS.MF_CHECKED : MENU_ITEM_FLAGS.MF_UNCHECKED), (uint)TrayCommands.TC_MODE_PASSIVE, "Off (keep using the selected power plan)"); - PInvoke.InsertMenu(modeMenu, 1, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING | (mode == AwakeMode.INDEFINITE ? MENU_ITEM_FLAGS.MF_CHECKED : MENU_ITEM_FLAGS.MF_UNCHECKED), (uint)TrayCommands.TC_MODE_INDEFINITE, "Keep awake indefinitely"); + PInvoke.InsertMenu(TrayMenu, 0, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_SEPARATOR, 0, string.Empty); - PInvoke.InsertMenu(modeMenu, 2, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_POPUP | (mode == AwakeMode.TIMED ? MENU_ITEM_FLAGS.MF_CHECKED : MENU_ITEM_FLAGS.MF_UNCHECKED), (uint)awakeTimeMenu.DangerousGetHandle(), "Keep awake temporarily"); - PInvoke.InsertMenu(TrayMenu, 0, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_POPUP, (uint)modeMenu.DangerousGetHandle(), "Mode"); + PInvoke.InsertMenu(TrayMenu, 0, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING | (mode == AwakeMode.PASSIVE ? MENU_ITEM_FLAGS.MF_CHECKED : MENU_ITEM_FLAGS.MF_UNCHECKED), (uint)TrayCommands.TC_MODE_PASSIVE, "Off (keep using the selected power plan)"); + PInvoke.InsertMenu(TrayMenu, 0, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING | (mode == AwakeMode.INDEFINITE ? MENU_ITEM_FLAGS.MF_CHECKED : MENU_ITEM_FLAGS.MF_UNCHECKED), (uint)TrayCommands.TC_MODE_INDEFINITE, "Keep awake indefinitely"); + PInvoke.InsertMenu(TrayMenu, 0, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_POPUP | (mode == AwakeMode.TIMED ? MENU_ITEM_FLAGS.MF_CHECKED : MENU_ITEM_FLAGS.MF_UNCHECKED), (uint)awakeTimeMenu.DangerousGetHandle(), "Keep awake on interval"); + PInvoke.InsertMenu(TrayMenu, 0, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING | MENU_ITEM_FLAGS.MF_DISABLED | (mode == AwakeMode.EXPIRABLE ? MENU_ITEM_FLAGS.MF_CHECKED : MENU_ITEM_FLAGS.MF_UNCHECKED), (uint)TrayCommands.TC_MODE_EXPIRABLE, "Keep awake until expiration date and time"); TrayIcon.Text = text; } diff --git a/src/modules/awake/Awake/Core/TrayMessageFilter.cs b/src/modules/awake/Awake/Core/TrayMessageFilter.cs index 06e07e1031..f7e86d71fa 100644 --- a/src/modules/awake/Awake/Core/TrayMessageFilter.cs +++ b/src/modules/awake/Awake/Core/TrayMessageFilter.cs @@ -56,13 +56,13 @@ namespace Awake.Core // Format for the timer block: // TrayCommands.TC_TIME + ZERO_BASED_INDEX_IN_SETTINGS AwakeSettings settings = ModuleSettings.GetSettings(InternalConstants.AppName); - if (settings.Properties.TrayTimeShortcuts.Count == 0) + if (settings.Properties.CustomTrayTimes.Count == 0) { - settings.Properties.TrayTimeShortcuts.AddRange(APIHelper.GetDefaultTrayOptions()); + settings.Properties.CustomTrayTimes.AddRange(APIHelper.GetDefaultTrayOptions()); } int index = (int)targetCommandIndex - (int)TrayCommands.TC_TIME; - var targetTime = settings.Properties.TrayTimeShortcuts.ElementAt(index).Value; + var targetTime = settings.Properties.CustomTrayTimes.ElementAt(index).Value; TimedKeepAwakeCommandHandler(InternalConstants.AppName, targetTime); break; } @@ -112,8 +112,8 @@ namespace Awake.Core } currentSettings.Properties.Mode = AwakeMode.TIMED; - currentSettings.Properties.Hours = (uint)timeSpan.Hours; - currentSettings.Properties.Minutes = (uint)timeSpan.Minutes; + currentSettings.Properties.IntervalHours = (uint)timeSpan.Hours; + currentSettings.Properties.IntervalMinutes = (uint)timeSpan.Minutes; ModuleSettings.SaveSettings(JsonSerializer.Serialize(currentSettings), moduleName); } diff --git a/src/modules/awake/Awake/NLog.config b/src/modules/awake/Awake/NLog.config index 13d89675d0..aaa21c75c5 100644 --- a/src/modules/awake/Awake/NLog.config +++ b/src/modules/awake/Awake/NLog.config @@ -2,7 +2,7 @@ - + ( + Option configOption = new( aliases: new[] { "--use-pt-config", "-c" }, getDefaultValue: () => false, description: $"Specifies whether {InternalConstants.AppName} will be using the PowerToys configuration file for managing the state.") @@ -106,11 +106,10 @@ namespace Awake { Arity = ArgumentArity.ZeroOrOne, }, + Required = false, }; - configOption.Required = false; - - var displayOption = new Option( + Option displayOption = new( aliases: new[] { "--display-on", "-d" }, getDefaultValue: () => true, description: "Determines whether the display should be kept awake.") @@ -119,11 +118,10 @@ namespace Awake { Arity = ArgumentArity.ZeroOrOne, }, + Required = false, }; - displayOption.Required = false; - - var timeOption = new Option( + Option timeOption = new( aliases: new[] { "--time-limit", "-t" }, getDefaultValue: () => 0, description: "Determines the interval, in seconds, during which the computer is kept awake.") @@ -132,34 +130,45 @@ namespace Awake { Arity = ArgumentArity.ExactlyOne, }, + Required = false, }; - timeOption.Required = false; - - var pidOption = new Option( + Option pidOption = new( aliases: new[] { "--pid", "-p" }, getDefaultValue: () => 0, - description: $"Bind the execution of {InternalConstants.AppName} to another process.") + description: $"Bind the execution of {InternalConstants.AppName} to another process. When the process ends, the system will resume managing the current sleep/display mode.") { Argument = new Argument(() => 0) { Arity = ArgumentArity.ZeroOrOne, }, + Required = false, }; - pidOption.Required = false; + Option expireAtOption = new( + aliases: new[] { "--expire-at", "-e" }, + getDefaultValue: () => string.Empty, + description: $"Determines the end date/time when {InternalConstants.AppName} will back off and let the system manage the current sleep/display mode.") + { + Argument = new Argument(() => string.Empty) + { + Arity = ArgumentArity.ZeroOrOne, + }, + Required = false, + }; - RootCommand? rootCommand = new RootCommand + RootCommand? rootCommand = new() { configOption, displayOption, timeOption, pidOption, + expireAtOption, }; rootCommand.Description = InternalConstants.AppName; - rootCommand.Handler = CommandHandler.Create(HandleCommandLineArguments); + rootCommand.Handler = CommandHandler.Create(HandleCommandLineArguments); _log.Info("Parameter setup complete. Proceeding to the rest of the app initiation..."); @@ -180,7 +189,7 @@ namespace Awake APIHelper.CompleteExit(exitCode, exitSignal, force); } - private static void HandleCommandLineArguments(bool usePtConfig, bool displayOn, uint timeLimit, int pid) + private static void HandleCommandLineArguments(bool usePtConfig, bool displayOn, uint timeLimit, int pid, string expireAt) { _handler += ExitHandler; APIHelper.SetConsoleControlHandler(_handler, true); @@ -199,6 +208,7 @@ namespace Awake _log.Info($"The value for --display-on is: {displayOn}"); _log.Info($"The value for --time-limit is: {timeLimit}"); _log.Info($"The value for --pid is: {pid}"); + _log.Info($"The value for --expire is: {expireAt}"); if (usePtConfig) { @@ -214,41 +224,21 @@ namespace Awake Exit("Received a signal to end the process. Making sure we quit...", 0, _exitSignal, true); } }).Start(); - TrayHelper.InitializeTray(InternalConstants.FullAppName, new Icon("modules/awake/images/awake.ico"), _exitSignal); + + TrayHelper.InitializeTray(InternalConstants.FullAppName, new Icon(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "images/awake.ico")), _exitSignal); string? settingsPath = _settingsUtils.GetSettingsFilePath(InternalConstants.AppName); _log.Info($"Reading configuration file: {settingsPath}"); - _watcher = new FileSystemWatcher + if (!File.Exists(settingsPath)) { -#pragma warning disable CS8601 // Possible null reference assignment. - Path = Path.GetDirectoryName(settingsPath), -#pragma warning restore CS8601 // Possible null reference assignment. - EnableRaisingEvents = true, - NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime, - Filter = Path.GetFileName(settingsPath), - }; + string? errorString = $"The settings file does not exist. Scaffolding default configuration..."; - IObservable>? changedObservable = Observable.FromEventPattern( - h => _watcher.Changed += h, - h => _watcher.Changed -= h); + AwakeSettings scaffoldSettings = new AwakeSettings(); + _settingsUtils.SaveSettings(JsonSerializer.Serialize(scaffoldSettings), InternalConstants.AppName); + } - IObservable>? createdObservable = Observable.FromEventPattern( - cre => _watcher.Created += cre, - cre => _watcher.Created -= cre); - - IObservable>? mergedObservable = Observable.Merge(changedObservable, createdObservable); - - mergedObservable.Throttle(TimeSpan.FromMilliseconds(25)) - .SubscribeOn(TaskPoolScheduler.Default) - .Select(e => e.EventArgs) - .Subscribe(HandleAwakeConfigChange); - - TrayHelper.SetTray(InternalConstants.FullAppName, new AwakeSettings(), _startedFromPowerToys); - - // Initially the file might not be updated, so we need to start processing - // settings right away. - ProcessSettings(); + ScaffoldConfiguration(settingsPath); } catch (Exception ex) { @@ -259,15 +249,43 @@ namespace Awake } else { - AwakeMode mode = timeLimit <= 0 ? AwakeMode.INDEFINITE : AwakeMode.TIMED; - - if (mode == AwakeMode.INDEFINITE) + // Date-based binding takes precedence over timed configuration, so we want to + // check for that first. + if (!string.IsNullOrWhiteSpace(expireAt)) { - SetupIndefiniteKeepAwake(displayOn); + try + { + DateTime expirationDateTime = DateTime.Parse(expireAt); + if (expirationDateTime > DateTime.Now) + { + // We want to have a dedicated expirable keep-awake logic instead of + // converting the target date to seconds and then passing to SetupTimedKeepAwake + // because that way we're accounting for the user potentially changing their clock + // while Awake is running. + SetupExpirableKeepAwake(expirationDateTime, displayOn); + } + else + { + _log.Info($"Target date is not in the future, therefore there is nothing to wait for."); + } + } + catch + { + _log.Error($"Could not parse date string {expireAt} into a viable date."); + } } else { - SetupTimedKeepAwake(timeLimit, displayOn); + AwakeMode mode = timeLimit <= 0 ? AwakeMode.INDEFINITE : AwakeMode.TIMED; + + if (mode == AwakeMode.INDEFINITE) + { + SetupIndefiniteKeepAwake(displayOn); + } + else + { + SetupTimedKeepAwake(timeLimit, displayOn); + } } } @@ -283,6 +301,45 @@ namespace Awake _exitSignal.WaitOne(); } + private static void ScaffoldConfiguration(string settingsPath) + { + try + { + _watcher = new FileSystemWatcher + { + Path = Path.GetDirectoryName(settingsPath)!, + EnableRaisingEvents = true, + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime, + Filter = Path.GetFileName(settingsPath), + }; + + IObservable>? changedObservable = Observable.FromEventPattern( + h => _watcher.Changed += h, + h => _watcher.Changed -= h); + + IObservable>? createdObservable = Observable.FromEventPattern( + cre => _watcher.Created += cre, + cre => _watcher.Created -= cre); + + IObservable>? mergedObservable = Observable.Merge(changedObservable, createdObservable); + + mergedObservable.Throttle(TimeSpan.FromMilliseconds(25)) + .SubscribeOn(TaskPoolScheduler.Default) + .Select(e => e.EventArgs) + .Subscribe(HandleAwakeConfigChange); + + TrayHelper.SetTray(InternalConstants.FullAppName, new AwakeSettings(), _startedFromPowerToys); + + // Initially the file might not be updated, so we need to start processing + // settings right away. + ProcessSettings(); + } + catch (Exception ex) + { + _log.Error($"An error occurred scaffolding the configuration. Error details: {ex.Message}"); + } + } + private static void SetupIndefiniteKeepAwake(bool displayOn) { APIHelper.SetIndefiniteKeepAwake(LogCompletedKeepAwakeThread, LogUnexpectedOrCancelledKeepAwakeThreadCompletion, displayOn); @@ -303,7 +360,7 @@ namespace Awake if (settings != null) { - _log.Info($"Identified custom time shortcuts for the tray: {settings.Properties.TrayTimeShortcuts.Count}"); + _log.Info($"Identified custom time shortcuts for the tray: {settings.Properties.CustomTrayTimes.Count}"); switch (settings.Properties.Mode) { @@ -321,12 +378,19 @@ namespace Awake case AwakeMode.TIMED: { - uint computedTime = (settings.Properties.Hours * 60 * 60) + (settings.Properties.Minutes * 60); + uint computedTime = (settings.Properties.IntervalHours * 60 * 60) + (settings.Properties.IntervalMinutes * 60); SetupTimedKeepAwake(computedTime, settings.Properties.KeepDisplayOn); break; } + case AwakeMode.EXPIRABLE: + { + SetupExpirableKeepAwake(settings.Properties.ExpirationDateTime, settings.Properties.KeepDisplayOn); + + break; + } + default: { string? errorMessage = "Unknown mode of operation. Check config file."; @@ -360,6 +424,13 @@ namespace Awake APIHelper.SetNoKeepAwake(); } + private static void SetupExpirableKeepAwake(DateTimeOffset expireAt, bool displayOn) + { + _log.Info($"Expirable keep-awake. Expected expiration date/time: {expireAt} with display on setting set to {displayOn}."); + + APIHelper.SetExpirableKeepAwake(expireAt, LogCompletedKeepAwakeThread, LogUnexpectedOrCancelledKeepAwakeThreadCompletion, displayOn); + } + private static void SetupTimedKeepAwake(uint time, bool displayOn) { _log.Info($"Timed keep-awake. Expected runtime: {time} seconds with display on setting set to {displayOn}."); @@ -369,14 +440,14 @@ namespace Awake private static void LogUnexpectedOrCancelledKeepAwakeThreadCompletion() { - string? errorMessage = "The keep-awake thread was terminated early."; + string? errorMessage = "The keep awake thread was terminated early."; _log.Info(errorMessage); _log.Debug(errorMessage); } - private static void LogCompletedKeepAwakeThread(bool result) + private static void LogCompletedKeepAwakeThread() { - _log.Info($"Exited keep-awake thread successfully: {result}"); + _log.Info($"Exited keep awake thread successfully."); } } } diff --git a/src/modules/awake/Awake/Telemetry/AwakeExpirableKeepAwakeEvent.cs b/src/modules/awake/Awake/Telemetry/AwakeExpirableKeepAwakeEvent.cs new file mode 100644 index 0000000000..114183dca0 --- /dev/null +++ b/src/modules/awake/Awake/Telemetry/AwakeExpirableKeepAwakeEvent.cs @@ -0,0 +1,16 @@ +// 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.Diagnostics.Tracing; +using Microsoft.PowerToys.Telemetry; +using Microsoft.PowerToys.Telemetry.Events; + +namespace Awake.Telemetry +{ + [EventData] + public class AwakeExpirableKeepAwakeEvent : EventBase, IEvent + { + public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage; + } +} diff --git a/src/modules/awake/Awake/Telemetry/AwakeNoKeepAwakeEvent.cs b/src/modules/awake/Awake/Telemetry/AwakeNoKeepAwakeEvent.cs new file mode 100644 index 0000000000..f9a696d8be --- /dev/null +++ b/src/modules/awake/Awake/Telemetry/AwakeNoKeepAwakeEvent.cs @@ -0,0 +1,16 @@ +// 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.Diagnostics.Tracing; +using Microsoft.PowerToys.Telemetry; +using Microsoft.PowerToys.Telemetry.Events; + +namespace Awake.Telemetry +{ + [EventData] + internal class AwakeNoKeepAwakeEvent : EventBase, IEvent + { + public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage; + } +} diff --git a/src/settings-ui/Settings.UI.Library/AwakeProperties.cs b/src/settings-ui/Settings.UI.Library/AwakeProperties.cs index 6db754d12b..e83161f8e6 100644 --- a/src/settings-ui/Settings.UI.Library/AwakeProperties.cs +++ b/src/settings-ui/Settings.UI.Library/AwakeProperties.cs @@ -2,6 +2,7 @@ // 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.Text.Json.Serialization; @@ -13,25 +14,29 @@ namespace Microsoft.PowerToys.Settings.UI.Library { KeepDisplayOn = false; Mode = AwakeMode.PASSIVE; - Hours = 0; - Minutes = 0; - TrayTimeShortcuts = new Dictionary(); + IntervalHours = 0; + IntervalMinutes = 0; + ExpirationDateTime = DateTimeOffset.MinValue; + CustomTrayTimes = new Dictionary(); } - [JsonPropertyName("awake_keep_display_on")] + [JsonPropertyName("keepDisplayOn")] public bool KeepDisplayOn { get; set; } - [JsonPropertyName("awake_mode")] + [JsonPropertyName("mode")] public AwakeMode Mode { get; set; } - [JsonPropertyName("awake_hours")] - public uint Hours { get; set; } + [JsonPropertyName("intervalHours")] + public uint IntervalHours { get; set; } - [JsonPropertyName("awake_minutes")] - public uint Minutes { get; set; } + [JsonPropertyName("intervalMinutes")] + public uint IntervalMinutes { get; set; } - [JsonPropertyName("tray_times")] - public Dictionary TrayTimeShortcuts { get; set; } + [JsonPropertyName("expirationDateTime")] + public DateTimeOffset ExpirationDateTime { get; set; } + + [JsonPropertyName("customTrayTimes")] + public Dictionary CustomTrayTimes { get; set; } } public enum AwakeMode @@ -39,5 +44,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library PASSIVE = 0, INDEFINITE = 1, TIMED = 2, + EXPIRABLE = 3, } } diff --git a/src/settings-ui/Settings.UI.Library/AwakeSettings.cs b/src/settings-ui/Settings.UI.Library/AwakeSettings.cs index b14c532ff5..e47cf82bc0 100644 --- a/src/settings-ui/Settings.UI.Library/AwakeSettings.cs +++ b/src/settings-ui/Settings.UI.Library/AwakeSettings.cs @@ -2,15 +2,17 @@ // 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.Linq; using System.Text.Json.Serialization; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; namespace Microsoft.PowerToys.Settings.UI.Library { - public class AwakeSettings : BasePTModuleSettings, ISettingsConfig + public class AwakeSettings : BasePTModuleSettings, ISettingsConfig, ICloneable { public const string ModuleName = "Awake"; - public const string ModuleVersion = "0.0.1"; + public const string ModuleVersion = "0.0.2"; public AwakeSettings() { @@ -22,6 +24,24 @@ namespace Microsoft.PowerToys.Settings.UI.Library [JsonPropertyName("properties")] public AwakeProperties Properties { get; set; } + public object Clone() + { + return new AwakeSettings() + { + Name = Name, + Version = Version, + Properties = new AwakeProperties() + { + CustomTrayTimes = Properties.CustomTrayTimes.ToDictionary(entry => entry.Key, entry => entry.Value), + Mode = Properties.Mode, + KeepDisplayOn = Properties.KeepDisplayOn, + IntervalMinutes = Properties.IntervalMinutes, + IntervalHours = Properties.IntervalHours, + ExpirationDateTime = Properties.ExpirationDateTime, + }, + }; + } + public string GetModuleName() { return Name; diff --git a/src/settings-ui/Settings.UI.Library/Interfaces/ISettingsRepository`1.cs b/src/settings-ui/Settings.UI.Library/Interfaces/ISettingsRepository`1.cs index 221a5d04a1..a9cd92899a 100644 --- a/src/settings-ui/Settings.UI.Library/Interfaces/ISettingsRepository`1.cs +++ b/src/settings-ui/Settings.UI.Library/Interfaces/ISettingsRepository`1.cs @@ -7,5 +7,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library.Interfaces public interface ISettingsRepository { T SettingsConfig { get; set; } + + bool ReloadSettings(); } } diff --git a/src/settings-ui/Settings.UI.Library/SettingsRepository`1.cs b/src/settings-ui/Settings.UI.Library/SettingsRepository`1.cs index 9cc9931284..b9d27dedb9 100644 --- a/src/settings-ui/Settings.UI.Library/SettingsRepository`1.cs +++ b/src/settings-ui/Settings.UI.Library/SettingsRepository`1.cs @@ -44,6 +44,23 @@ namespace Microsoft.PowerToys.Settings.UI.Library { } + public bool ReloadSettings() + { + try + { + T settingsItem = new T(); + settingsConfig = _settingsUtils.GetSettingsOrDefault(settingsItem.GetModuleName()); + + SettingsConfig = settingsConfig; + + return true; + } + catch + { + return false; + } + } + // Settings configurations shared across all viewmodels public T SettingsConfig { diff --git a/src/settings-ui/Settings.UI.UnitTests/BackwardsCompatibility/BackCompatTestProperties.cs b/src/settings-ui/Settings.UI.UnitTests/BackwardsCompatibility/BackCompatTestProperties.cs index 1a702b38ae..7827023cc2 100644 --- a/src/settings-ui/Settings.UI.UnitTests/BackwardsCompatibility/BackCompatTestProperties.cs +++ b/src/settings-ui/Settings.UI.UnitTests/BackwardsCompatibility/BackCompatTestProperties.cs @@ -48,6 +48,23 @@ namespace Microsoft.PowerToys.Settings.UI.UnitTests.BackwardsCompatibility } } } + + public bool ReloadSettings() + { + try + { + T settingsItem = new T(); + _settingsConfig = _settingsUtils.GetSettingsOrDefault(settingsItem.GetModuleName()); + + SettingsConfig = _settingsConfig; + + return true; + } + catch + { + return false; + } + } } public static Mock GetModuleIOProvider(string version, string module, string fileName) diff --git a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw index 8740403ac7..3473d8344e 100644 --- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw +++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw @@ -1812,35 +1812,41 @@ From there, simply click on one of the supported files in the File Explorer and A convenient way to keep your PC awake on-demand. - + Enable Awake Awake is a product name, do not loc - + Keep using the selected power plan - + Keep awake indefinitely - - Keep awake temporarily + + Keep awake for a time interval - + + Keep awake until expiration + + Keep screen on - + This setting is only available when keeping the PC awake - + + Keep custom awakeness state until a specific date and time + + Mode - + Behavior - + Hours - + Minutes @@ -1930,7 +1936,7 @@ From there, simply click on one of the supported files in the File Explorer and See what's new - + Manage the state of your device when Awake is active @@ -2120,8 +2126,11 @@ From there, simply click on one of the supported files in the File Explorer and New size First part of the default name of new sizes that can be added in PT's settings ui. - - Time before returning to the previous awakeness state + + Interval before returning to the previous awakeness state + + + End date and time Mouse utilities @@ -3056,4 +3065,4 @@ Activate by holding the key for the character you want to add an accent to, then Consider loopback addresses as duplicates - \ No newline at end of file + diff --git a/src/settings-ui/Settings.UI/ViewModels/AwakeViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/AwakeViewModel.cs index 1b122f32b6..28dd371fcc 100644 --- a/src/settings-ui/Settings.UI/ViewModels/AwakeViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/AwakeViewModel.cs @@ -4,62 +4,29 @@ using System; using System.Runtime.CompilerServices; -using global::PowerToys.GPOWrapper; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.Helpers; -using Microsoft.PowerToys.Settings.UI.Library.Interfaces; +using Microsoft.PowerToys.Settings.UI.Library.Utilities; namespace Microsoft.PowerToys.Settings.UI.ViewModels { public class AwakeViewModel : Observable { - private GeneralSettings GeneralSettingsConfig { get; set; } - - private AwakeSettings Settings { get; set; } - - private Func SendConfigMSG { get; } - - public AwakeViewModel(ISettingsRepository settingsRepository, ISettingsRepository moduleSettingsRepository, Func ipcMSGCallBackFunc) + public AwakeViewModel() { - // To obtain the general settings configurations of PowerToys Settings. - if (settingsRepository == null) - { - throw new ArgumentNullException(nameof(settingsRepository)); - } - - GeneralSettingsConfig = settingsRepository.SettingsConfig; - - // To obtain the settings configurations of Fancy zones. - if (moduleSettingsRepository == null) - { - throw new ArgumentNullException(nameof(moduleSettingsRepository)); - } - - Settings = moduleSettingsRepository.SettingsConfig; - - InitializeEnabledValue(); - - _keepDisplayOn = Settings.Properties.KeepDisplayOn; - _mode = Settings.Properties.Mode; - _hours = Settings.Properties.Hours; - _minutes = Settings.Properties.Minutes; - - // set the callback functions value to hangle outgoing IPC message. - SendConfigMSG = ipcMSGCallBackFunc; } - private void InitializeEnabledValue() + public AwakeSettings ModuleSettings { - _enabledGpoRuleConfiguration = GPOWrapper.GetConfiguredAwakeEnabledValue(); - if (_enabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled) + get => _moduleSettings; + set { - // Get the enabled state from GPO. - _enabledStateIsGPOConfigured = true; - _isEnabled = _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled; - } - else - { - _isEnabled = GeneralSettingsConfig.Enabled.Awake; + if (_moduleSettings != value) + { + _moduleSettings = value; + RefreshModuleSettings(); + RefreshEnabledState(); + } } } @@ -78,13 +45,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels _isEnabled = value; - GeneralSettingsConfig.Enabled.Awake = value; - OnPropertyChanged(nameof(IsEnabled)); - OnPropertyChanged(nameof(IsTimeConfigurationEnabled)); - OnPropertyChanged(nameof(IsScreenConfigurationPossibleEnabled)); + RefreshEnabledState(); - OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig); - SendConfigMSG(outgoing.ToString()); NotifyPropertyChanged(); } } @@ -93,31 +55,44 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels public bool IsEnabledGpoConfigured { get => _enabledStateIsGPOConfigured; + set + { + if (_enabledStateIsGPOConfigured != value) + { + _enabledStateIsGPOConfigured = value; + NotifyPropertyChanged(); + } + } + } + + public bool IsExpirationConfigurationEnabled + { + get => ModuleSettings.Properties.Mode == AwakeMode.EXPIRABLE && IsEnabled; } public bool IsTimeConfigurationEnabled { - get => _mode == AwakeMode.TIMED && _isEnabled; + get => ModuleSettings.Properties.Mode == AwakeMode.TIMED && IsEnabled; } public bool IsScreenConfigurationPossibleEnabled { - get => _mode != AwakeMode.PASSIVE && _isEnabled; + get => ModuleSettings.Properties.Mode != AwakeMode.PASSIVE && IsEnabled; } public AwakeMode Mode { - get => _mode; + get => ModuleSettings.Properties.Mode; set { - if (_mode != value) + if (ModuleSettings.Properties.Mode != value) { - _mode = value; - OnPropertyChanged(nameof(Mode)); + ModuleSettings.Properties.Mode = value; + OnPropertyChanged(nameof(IsTimeConfigurationEnabled)); OnPropertyChanged(nameof(IsScreenConfigurationPossibleEnabled)); + OnPropertyChanged(nameof(IsExpirationConfigurationEnabled)); - Settings.Properties.Mode = value; NotifyPropertyChanged(); } } @@ -125,79 +100,93 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels public bool KeepDisplayOn { - get => _keepDisplayOn; + get => ModuleSettings.Properties.KeepDisplayOn; set { - if (_keepDisplayOn != value) + if (ModuleSettings.Properties.KeepDisplayOn != value) { - _keepDisplayOn = value; - OnPropertyChanged(nameof(KeepDisplayOn)); - - Settings.Properties.KeepDisplayOn = value; + ModuleSettings.Properties.KeepDisplayOn = value; NotifyPropertyChanged(); } } } - public uint Hours + public uint IntervalHours { - get => _hours; + get => ModuleSettings.Properties.IntervalHours; set { - if (_hours != value) + if (ModuleSettings.Properties.IntervalHours != value) { - _hours = value; - OnPropertyChanged(nameof(Hours)); - - Settings.Properties.Hours = value; + ModuleSettings.Properties.IntervalHours = value; NotifyPropertyChanged(); } } } - public uint Minutes + public uint IntervalMinutes { - get => _minutes; + get => ModuleSettings.Properties.IntervalMinutes; set { - if (_minutes != value) + if (ModuleSettings.Properties.IntervalMinutes != value) { - _minutes = value; - OnPropertyChanged(nameof(Minutes)); - - Settings.Properties.Minutes = value; + ModuleSettings.Properties.IntervalMinutes = value; NotifyPropertyChanged(); } } } + public DateTimeOffset ExpirationDateTime + { + get => ModuleSettings.Properties.ExpirationDateTime; + set + { + if (ModuleSettings.Properties.ExpirationDateTime != value) + { + ModuleSettings.Properties.ExpirationDateTime = value; + NotifyPropertyChanged(); + } + } + } + + public TimeSpan ExpirationTime + { + get => ExpirationDateTime.TimeOfDay; + set + { + if (ExpirationDateTime.TimeOfDay != value) + { + ExpirationDateTime = new DateTime(ExpirationDateTime.Year, ExpirationDateTime.Month, ExpirationDateTime.Day, value.Hours, value.Minutes, value.Seconds); + } + } + } + public void NotifyPropertyChanged([CallerMemberName] string propertyName = null) { + Logger.LogInfo($"Changed the property {propertyName}"); OnPropertyChanged(propertyName); - if (SendConfigMSG != null) - { - SndAwakeSettings outsettings = new SndAwakeSettings(Settings); - SndModuleSettings ipcMessage = new SndModuleSettings(outsettings); - - string targetMessage = ipcMessage.ToJsonString(); - SendConfigMSG(targetMessage); - } } public void RefreshEnabledState() { - InitializeEnabledValue(); OnPropertyChanged(nameof(IsEnabled)); OnPropertyChanged(nameof(IsTimeConfigurationEnabled)); OnPropertyChanged(nameof(IsScreenConfigurationPossibleEnabled)); + OnPropertyChanged(nameof(IsExpirationConfigurationEnabled)); + } + + public void RefreshModuleSettings() + { + OnPropertyChanged(nameof(Mode)); + OnPropertyChanged(nameof(KeepDisplayOn)); + OnPropertyChanged(nameof(IntervalHours)); + OnPropertyChanged(nameof(IntervalMinutes)); + OnPropertyChanged(nameof(ExpirationDateTime)); } - private GpoRuleConfigured _enabledGpoRuleConfiguration; private bool _enabledStateIsGPOConfigured; + private AwakeSettings _moduleSettings; private bool _isEnabled; - private uint _hours; - private uint _minutes; - private bool _keepDisplayOn; - private AwakeMode _mode; } } diff --git a/src/settings-ui/Settings.UI/Views/AwakePage.xaml b/src/settings-ui/Settings.UI/Views/AwakePage.xaml index 74030677b3..29823d4fe7 100644 --- a/src/settings-ui/Settings.UI/Views/AwakePage.xaml +++ b/src/settings-ui/Settings.UI/Views/AwakePage.xaml @@ -8,6 +8,8 @@ xmlns:labs="using:CommunityToolkit.Labs.WinUI" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:ui="using:CommunityToolkit.WinUI.UI" + xmlns:viewmodels="using:Microsoft.PowerToys.Settings.UI.ViewModels" + d:DataContext="{d:DesignInstance Type=viewmodels:AwakeViewModel}" AutomationProperties.LandmarkType="Main" mc:Ignorable="d"> @@ -22,7 +24,7 @@ - - - + + + + + + + + + + + + Value="{x:Bind ViewModel.IntervalHours, Mode=TwoWay}" /> + Value="{x:Bind ViewModel.IntervalMinutes, Mode=TwoWay}" /> @@ -99,7 +111,7 @@ diff --git a/src/settings-ui/Settings.UI/Views/AwakePage.xaml.cs b/src/settings-ui/Settings.UI/Views/AwakePage.xaml.cs index ec3abd4806..33effd94b5 100644 --- a/src/settings-ui/Settings.UI/Views/AwakePage.xaml.cs +++ b/src/settings-ui/Settings.UI/Views/AwakePage.xaml.cs @@ -2,25 +2,184 @@ // 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.IO; +using System.IO.Abstractions; using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.PowerToys.Settings.UI.Library.Interfaces; +using Microsoft.PowerToys.Settings.UI.Library.Utilities; using Microsoft.PowerToys.Settings.UI.ViewModels; +using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml.Controls; +using PowerToys.GPOWrapper; namespace Microsoft.PowerToys.Settings.UI.Views { public sealed partial class AwakePage : Page, IRefreshablePage { + private readonly string _appName = "Awake"; + private readonly SettingsUtils _settingsUtils; + + private readonly SettingsRepository _generalSettingsRepository; + private readonly SettingsRepository _moduleSettingsRepository; + + private readonly IFileSystem _fileSystem; + private readonly IFileSystemWatcher _fileSystemWatcher; + + private readonly DispatcherQueue _dispatcherQueue; + + private readonly Func _sendConfigMsg; + private AwakeViewModel ViewModel { get; set; } public AwakePage() { - var settingsUtils = new SettingsUtils(); - ViewModel = new AwakeViewModel(SettingsRepository.GetInstance(settingsUtils), SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage); + _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + _fileSystem = new FileSystem(); + _settingsUtils = new SettingsUtils(); + _sendConfigMsg = ShellPage.SendDefaultIPCMessage; + + ViewModel = new AwakeViewModel(); + ViewModel.PropertyChanged += ViewModel_PropertyChanged; + + _generalSettingsRepository = SettingsRepository.GetInstance(_settingsUtils); + _moduleSettingsRepository = SettingsRepository.GetInstance(_settingsUtils); + + // We load the view model settings first. + LoadSettings(_generalSettingsRepository, _moduleSettingsRepository); + DataContext = ViewModel; + + var settingsPath = _settingsUtils.GetSettingsFilePath(_appName); + + _fileSystemWatcher = _fileSystem.FileSystemWatcher.CreateNew(); + _fileSystemWatcher.Path = _fileSystem.Path.GetDirectoryName(settingsPath); + _fileSystemWatcher.Filter = _fileSystem.Path.GetFileName(settingsPath); + _fileSystemWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime; + _fileSystemWatcher.Changed += Settings_Changed; + _fileSystemWatcher.EnableRaisingEvents = true; + InitializeComponent(); } + /// + /// Triggered whenever a view model property changes. This is done in addition to the baked-in view model changes. + /// + /// + /// TODO: The logic here needs to be optimized since doing string comparison on values is not ideal. + /// + /// Sender of the change. + /// Property parameter. + private void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if (_sendConfigMsg != null) + { + if (e.PropertyName == "IsEnabled") + { + if (ViewModel.IsEnabled != _generalSettingsRepository.SettingsConfig.Enabled.Awake) + { + _generalSettingsRepository.SettingsConfig.Enabled.Awake = ViewModel.IsEnabled; + + var generalSettingsMessage = new OutGoingGeneralSettings(_generalSettingsRepository.SettingsConfig).ToString(); + + Logger.LogInfo($"Saved general settings from Awake page."); + _sendConfigMsg(generalSettingsMessage); + } + } + else + { + if (ViewModel.ModuleSettings != null) + { + SndAwakeSettings currentSettings = new(_moduleSettingsRepository.SettingsConfig); + SndModuleSettings csIpcMessage = new(currentSettings); + + SndAwakeSettings outSettings = new(ViewModel.ModuleSettings); + SndModuleSettings outIpcMessage = new(outSettings); + + string csMessage = csIpcMessage.ToJsonString(); + string outMessage = outIpcMessage.ToJsonString(); + + if (!csMessage.Equals(outMessage)) + { + Logger.LogInfo($"Saved Awake settings from Awake page."); + _sendConfigMsg(outMessage); + } + } + } + } + } + + private void LoadSettings(ISettingsRepository generalSettingsRepository, ISettingsRepository moduleSettingsRepository) + { + if (generalSettingsRepository != null) + { + if (moduleSettingsRepository != null) + { + UpdateViewModelSettings(moduleSettingsRepository.SettingsConfig, generalSettingsRepository.SettingsConfig); + } + else + { + throw new ArgumentNullException(nameof(moduleSettingsRepository)); + } + } + else + { + throw new ArgumentNullException(nameof(generalSettingsRepository)); + } + } + + private void UpdateViewModelSettings(AwakeSettings awakeSettings, GeneralSettings generalSettings) + { + if (awakeSettings != null) + { + if (generalSettings != null) + { + ViewModel.IsEnabled = generalSettings.Enabled.Awake; + ViewModel.ModuleSettings = (AwakeSettings)awakeSettings.Clone(); + + UpdateEnabledState(generalSettings.Enabled.Awake); + } + else + { + throw new ArgumentNullException(nameof(generalSettings)); + } + } + else + { + throw new ArgumentNullException(nameof(awakeSettings)); + } + } + + /// + /// Updates the tool enablement state. + /// + /// The state that is recommended for the tool, but can be overridden if a GPO policy is in place. + private void UpdateEnabledState(bool recommendedState) + { + var enabledGpoRuleConfiguration = GPOWrapper.GetConfiguredAwakeEnabledValue(); + + if (enabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled) + { + // Get the enabled state from GPO. + ViewModel.IsEnabledGpoConfigured = true; + ViewModel.IsEnabled = enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled; + } + else + { + ViewModel.IsEnabled = recommendedState; + } + } + + private void Settings_Changed(object sender, FileSystemEventArgs e) + { + bool taskAdded = _dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Normal, () => + { + _moduleSettingsRepository.ReloadSettings(); + LoadSettings(_generalSettingsRepository, _moduleSettingsRepository); + }); + } + public void RefreshEnabledState() { ViewModel.RefreshEnabledState();