diff --git a/SafeExamBrowser.Configuration/Settings/BrowserSettings.cs b/SafeExamBrowser.Configuration/BrowserSettings.cs similarity index 96% rename from SafeExamBrowser.Configuration/Settings/BrowserSettings.cs rename to SafeExamBrowser.Configuration/BrowserSettings.cs index 7fe549b6..d8f88fd0 100644 --- a/SafeExamBrowser.Configuration/Settings/BrowserSettings.cs +++ b/SafeExamBrowser.Configuration/BrowserSettings.cs @@ -10,7 +10,7 @@ using System; using System.IO; using SafeExamBrowser.Contracts.Configuration.Settings; -namespace SafeExamBrowser.Configuration.Settings +namespace SafeExamBrowser.Configuration { public class BrowserSettings : IBrowserSettings { diff --git a/SafeExamBrowser.Configuration/Settings/KeyboardSettings.cs b/SafeExamBrowser.Configuration/KeyboardSettings.cs similarity index 91% rename from SafeExamBrowser.Configuration/Settings/KeyboardSettings.cs rename to SafeExamBrowser.Configuration/KeyboardSettings.cs index e468d724..6513c957 100644 --- a/SafeExamBrowser.Configuration/Settings/KeyboardSettings.cs +++ b/SafeExamBrowser.Configuration/KeyboardSettings.cs @@ -8,7 +8,7 @@ using SafeExamBrowser.Contracts.Configuration.Settings; -namespace SafeExamBrowser.Configuration.Settings +namespace SafeExamBrowser.Configuration { public class KeyboardSettings : IKeyboardSettings { diff --git a/SafeExamBrowser.Configuration/Settings/MouseSettings.cs b/SafeExamBrowser.Configuration/MouseSettings.cs similarity index 83% rename from SafeExamBrowser.Configuration/Settings/MouseSettings.cs rename to SafeExamBrowser.Configuration/MouseSettings.cs index f911db24..52fb0cde 100644 --- a/SafeExamBrowser.Configuration/Settings/MouseSettings.cs +++ b/SafeExamBrowser.Configuration/MouseSettings.cs @@ -8,12 +8,12 @@ using SafeExamBrowser.Contracts.Configuration.Settings; -namespace SafeExamBrowser.Configuration.Settings +namespace SafeExamBrowser.Configuration { public class MouseSettings : IMouseSettings { public bool AllowMiddleButton => false; - public bool AllowRightButton => false; + public bool AllowRightButton => true; } } diff --git a/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj b/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj index e263f7dc..2aaba73b 100644 --- a/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj +++ b/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj @@ -60,12 +60,10 @@ - - - - - - + + + + diff --git a/SafeExamBrowser.Configuration/Settings/SettingsImpl.cs b/SafeExamBrowser.Configuration/Settings.cs similarity index 95% rename from SafeExamBrowser.Configuration/Settings/SettingsImpl.cs rename to SafeExamBrowser.Configuration/Settings.cs index f5ef343f..fdcb7c0e 100644 --- a/SafeExamBrowser.Configuration/Settings/SettingsImpl.cs +++ b/SafeExamBrowser.Configuration/Settings.cs @@ -9,7 +9,6 @@ using System; using System.IO; using System.Reflection; -using SafeExamBrowser.Configuration.Settings; using SafeExamBrowser.Contracts.Configuration.Settings; namespace SafeExamBrowser.Configuration @@ -17,11 +16,11 @@ namespace SafeExamBrowser.Configuration /// /// TODO: Replace with proper implementation once configuration aspects are clear... /// - public class SettingsImpl : ISettings + public class Settings : ISettings { private static readonly string LogFileDate = DateTime.Now.ToString("yyyy-MM-dd\\_HH\\hmm\\mss\\s"); - public SettingsImpl() + public Settings() { Browser = new BrowserSettings(this); Keyboard = new KeyboardSettings(); diff --git a/SafeExamBrowser.Configuration/WorkingArea.cs b/SafeExamBrowser.Configuration/WorkingArea.cs deleted file mode 100644 index 00f8d00c..00000000 --- a/SafeExamBrowser.Configuration/WorkingArea.cs +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2017 ETH Zürich, Educational Development and Technology (LET) - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -using System.Windows.Forms; -using SafeExamBrowser.Contracts.Configuration; -using SafeExamBrowser.Contracts.Logging; -using SafeExamBrowser.Contracts.UserInterface; -using SafeExamBrowser.Contracts.WindowsApi; - -namespace SafeExamBrowser.Configuration -{ - public class WorkingArea : IWorkingArea - { - private ILogger logger; - private INativeMethods nativeMethods; - private IBounds originalWorkingArea; - - public WorkingArea(ILogger logger, INativeMethods nativeMethods) - { - this.logger = logger; - this.nativeMethods = nativeMethods; - } - - public void InitializeFor(ITaskbar taskbar) - { - originalWorkingArea = nativeMethods.GetWorkingArea(); - - LogWorkingArea("Saved original working area", originalWorkingArea); - - var area = new Bounds - { - Left = 0, - Top = 0, - Right = Screen.PrimaryScreen.Bounds.Width, - Bottom = Screen.PrimaryScreen.Bounds.Height - taskbar.GetAbsoluteHeight() - }; - - LogWorkingArea("Trying to set new working area", area); - nativeMethods.SetWorkingArea(area); - LogWorkingArea("Working area is now set to", nativeMethods.GetWorkingArea()); - } - - public void Reset() - { - if (originalWorkingArea != null) - { - nativeMethods.SetWorkingArea(originalWorkingArea); - LogWorkingArea("Restored original working area", originalWorkingArea); - } - } - - private void LogWorkingArea(string message, IBounds area) - { - logger.Info($"{message}: Left = {area.Left}, Top = {area.Top}, Right = {area.Right}, Bottom = {area.Bottom}."); - } - } -} diff --git a/SafeExamBrowser.Contracts/Configuration/IWorkingArea.cs b/SafeExamBrowser.Contracts/Configuration/IWorkingArea.cs deleted file mode 100644 index 2b61ce86..00000000 --- a/SafeExamBrowser.Contracts/Configuration/IWorkingArea.cs +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) 2017 ETH Zürich, Educational Development and Technology (LET) - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -using SafeExamBrowser.Contracts.UserInterface; - -namespace SafeExamBrowser.Contracts.Configuration -{ - public interface IWorkingArea - { - /// - /// Sets the Windows working area to accommodate to the taskbar's dimensions. - /// - void InitializeFor(ITaskbar taskbar); - - /// - /// Resets the Windows working area to its previous (initial) state. - /// - void Reset(); - } -} diff --git a/SafeExamBrowser.Contracts/Monitoring/IDisplayMonitor.cs b/SafeExamBrowser.Contracts/Monitoring/IDisplayMonitor.cs new file mode 100644 index 00000000..8bd33d26 --- /dev/null +++ b/SafeExamBrowser.Contracts/Monitoring/IDisplayMonitor.cs @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2017 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +namespace SafeExamBrowser.Contracts.Monitoring +{ + public delegate void DisplayChangedEventHandler(); + + public interface IDisplayMonitor + { + /// + /// Event fired when the primary display or its settings have changed. + /// + event DisplayChangedEventHandler DisplayChanged; + + /// + /// Sets the desktop working area to accommodate to the taskbar's height, removes the configured wallpaper (if possible) and + /// prevents the computer from entering sleep mode or turning its display(s) off. + /// + void InitializePrimaryDisplay(int taskbarHeight); + + /// + /// Resets the desktop working area and wallpaper to their previous (initial) state. + /// + void ResetPrimaryDisplay(); + + /// + /// Starts monitoring for display changes, i.e. display changes will trigger the DisplaySettingsChanged event. + /// + void StartMonitoringDisplayChanges(); + + /// + /// Stops monitoring for display changes. + /// + void StopMonitoringDisplayChanges(); + } +} diff --git a/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj b/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj index 2bbdd10e..29432ff7 100644 --- a/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj +++ b/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj @@ -72,7 +72,6 @@ - @@ -83,6 +82,7 @@ + diff --git a/SafeExamBrowser.Contracts/WindowsApi/INativeMethods.cs b/SafeExamBrowser.Contracts/WindowsApi/INativeMethods.cs index a3c88c6a..f25a07f1 100644 --- a/SafeExamBrowser.Contracts/WindowsApi/INativeMethods.cs +++ b/SafeExamBrowser.Contracts/WindowsApi/INativeMethods.cs @@ -38,6 +38,11 @@ namespace SafeExamBrowser.Contracts.WindowsApi /// void DeregisterSystemEvent(IntPtr handle); + /// + /// Prevents Windows from entering sleep mode and keeps all displays powered on. + /// + void DisableSleep(); + /// /// Empties the clipboard. /// @@ -72,8 +77,15 @@ namespace SafeExamBrowser.Contracts.WindowsApi uint GetShellProcessId(); /// - /// Retrieves the title of the specified window, or an empty string, if the - /// given window does not have a title. + /// Retrieves the path of the currently configured wallpaper image, or an empty string, if there is no wallpaper set. + /// + /// + /// If the wallpaper path could not be retrieved. + /// + string GetWallpaperPath(); + + /// + /// Retrieves the title of the specified window, or an empty string, if the given window does not have a title. /// string GetWindowTitle(IntPtr window); @@ -125,6 +137,14 @@ namespace SafeExamBrowser.Contracts.WindowsApi /// IntPtr RegisterSystemCaptureStartEvent(Action callback); + /// + /// Removes the currently configured desktop wallpaper. + /// + /// + /// If the wallpaper could not be removed. + /// + void RemoveWallpaper(); + /// /// Restores the specified window to its original size and position. /// @@ -135,6 +155,14 @@ namespace SafeExamBrowser.Contracts.WindowsApi /// void SendCloseMessageTo(IntPtr window); + /// + /// Sets the wallpaper to the image located at the specified file path. + /// + /// + /// If the wallpaper could not be set. + /// + void SetWallpaper(string filePath); + /// /// Sets the working area of the primary screen according to the given dimensions. /// diff --git a/SafeExamBrowser.Core.UnitTests/Behaviour/RuntimeControllerTests.cs b/SafeExamBrowser.Core.UnitTests/Behaviour/RuntimeControllerTests.cs index 6aa694e3..60fdc12e 100644 --- a/SafeExamBrowser.Core.UnitTests/Behaviour/RuntimeControllerTests.cs +++ b/SafeExamBrowser.Core.UnitTests/Behaviour/RuntimeControllerTests.cs @@ -10,7 +10,6 @@ using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using SafeExamBrowser.Contracts.Behaviour; -using SafeExamBrowser.Contracts.Configuration; using SafeExamBrowser.Contracts.Logging; using SafeExamBrowser.Contracts.Monitoring; using SafeExamBrowser.Contracts.UserInterface; @@ -21,33 +20,52 @@ namespace SafeExamBrowser.Core.UnitTests.Behaviour [TestClass] public class RuntimeControllerTests { + private Mock displayMonitorMock; private Mock loggerMock; private Mock processMonitorMock; private Mock taskbarMock; private Mock windowMonitorMock; - private Mock workingAreaMock; private IRuntimeController sut; [TestInitialize] public void Initialize() { + displayMonitorMock = new Mock(); loggerMock = new Mock(); processMonitorMock = new Mock(); taskbarMock = new Mock(); windowMonitorMock= new Mock(); - workingAreaMock = new Mock(); sut = new RuntimeController( + displayMonitorMock.Object, loggerMock.Object, processMonitorMock.Object, taskbarMock.Object, - windowMonitorMock.Object, - workingAreaMock.Object); + windowMonitorMock.Object); sut.Start(); } + [TestMethod] + public void MustHandleDisplayChangeCorrectly() + { + var order = 0; + var workingArea = 0; + var taskbar = 0; + + displayMonitorMock.Setup(w => w.InitializePrimaryDisplay(taskbarMock.Object.GetAbsoluteHeight())).Callback(() => workingArea = ++order); + taskbarMock.Setup(t => t.InitializeBounds()).Callback(() => taskbar = ++order); + + displayMonitorMock.Raise(d => d.DisplayChanged += null); + + displayMonitorMock.Verify(w => w.InitializePrimaryDisplay(taskbarMock.Object.GetAbsoluteHeight()), Times.Once); + taskbarMock.Verify(t => t.InitializeBounds(), Times.Once); + + Assert.IsTrue(workingArea == 1); + Assert.IsTrue(taskbar == 2); + } + [TestMethod] public void MustHandleExplorerStartCorrectly() { @@ -57,13 +75,13 @@ namespace SafeExamBrowser.Core.UnitTests.Behaviour var taskbar = 0; processMonitorMock.Setup(p => p.CloseExplorerShell()).Callback(() => processManager = ++order); - workingAreaMock.Setup(w => w.InitializeFor(taskbarMock.Object)).Callback(() => workingArea = ++order); + displayMonitorMock.Setup(w => w.InitializePrimaryDisplay(taskbarMock.Object.GetAbsoluteHeight())).Callback(() => workingArea = ++order); taskbarMock.Setup(t => t.InitializeBounds()).Callback(() => taskbar = ++order); processMonitorMock.Raise(p => p.ExplorerStarted += null); processMonitorMock.Verify(p => p.CloseExplorerShell(), Times.Once); - workingAreaMock.Verify(w => w.InitializeFor(taskbarMock.Object), Times.Once); + displayMonitorMock.Verify(w => w.InitializePrimaryDisplay(taskbarMock.Object.GetAbsoluteHeight()), Times.Once); taskbarMock.Verify(t => t.InitializeBounds(), Times.Once); Assert.IsTrue(processManager == 1); diff --git a/SafeExamBrowser.Core/Behaviour/Operations/WorkingAreaOperation.cs b/SafeExamBrowser.Core/Behaviour/Operations/DisplayMonitorOperation.cs similarity index 67% rename from SafeExamBrowser.Core/Behaviour/Operations/WorkingAreaOperation.cs rename to SafeExamBrowser.Core/Behaviour/Operations/DisplayMonitorOperation.cs index 851bf2e0..8084ced5 100644 --- a/SafeExamBrowser.Core/Behaviour/Operations/WorkingAreaOperation.cs +++ b/SafeExamBrowser.Core/Behaviour/Operations/DisplayMonitorOperation.cs @@ -7,26 +7,26 @@ */ using SafeExamBrowser.Contracts.Behaviour; -using SafeExamBrowser.Contracts.Configuration; using SafeExamBrowser.Contracts.I18n; using SafeExamBrowser.Contracts.Logging; +using SafeExamBrowser.Contracts.Monitoring; using SafeExamBrowser.Contracts.UserInterface; namespace SafeExamBrowser.Core.Behaviour.Operations { - public class WorkingAreaOperation : IOperation + public class DisplayMonitorOperation : IOperation { + private IDisplayMonitor displayMonitor; private ILogger logger; private ITaskbar taskbar; - private IWorkingArea workingArea; public ISplashScreen SplashScreen { private get; set; } - public WorkingAreaOperation(ILogger logger, ITaskbar taskbar, IWorkingArea workingArea) + public DisplayMonitorOperation(IDisplayMonitor displayMonitor, ILogger logger, ITaskbar taskbar) { + this.displayMonitor = displayMonitor; this.logger = logger; this.taskbar = taskbar; - this.workingArea = workingArea; } public void Perform() @@ -34,10 +34,8 @@ namespace SafeExamBrowser.Core.Behaviour.Operations logger.Info("Initializing working area..."); SplashScreen.UpdateText(TextKey.SplashScreen_InitializeWorkingArea); - // TODO - // - Emptying clipboard - - workingArea.InitializeFor(taskbar); + displayMonitor.InitializePrimaryDisplay(taskbar.GetAbsoluteHeight()); + displayMonitor.StartMonitoringDisplayChanges(); } public void Revert() @@ -45,10 +43,8 @@ namespace SafeExamBrowser.Core.Behaviour.Operations logger.Info("Restoring working area..."); SplashScreen.UpdateText(TextKey.SplashScreen_RestoreWorkingArea); - // TODO - // - Emptying clipboard - - workingArea.Reset(); + displayMonitor.StopMonitoringDisplayChanges(); + displayMonitor.ResetPrimaryDisplay(); } } } diff --git a/SafeExamBrowser.Core/Behaviour/RuntimeController.cs b/SafeExamBrowser.Core/Behaviour/RuntimeController.cs index 6b2788ef..2b1abd78 100644 --- a/SafeExamBrowser.Core/Behaviour/RuntimeController.cs +++ b/SafeExamBrowser.Core/Behaviour/RuntimeController.cs @@ -8,7 +8,6 @@ using System; using SafeExamBrowser.Contracts.Behaviour; -using SafeExamBrowser.Contracts.Configuration; using SafeExamBrowser.Contracts.Logging; using SafeExamBrowser.Contracts.Monitoring; using SafeExamBrowser.Contracts.UserInterface; @@ -17,28 +16,29 @@ namespace SafeExamBrowser.Core.Behaviour { public class RuntimeController : IRuntimeController { + private IDisplayMonitor displayMonitor; private ILogger logger; private IProcessMonitor processMonitor; private ITaskbar taskbar; private IWindowMonitor windowMonitor; - private IWorkingArea workingArea; public RuntimeController( + IDisplayMonitor displayMonitor, ILogger logger, IProcessMonitor processMonitor, ITaskbar taskbar, - IWindowMonitor windowMonitor, - IWorkingArea workingArea) + IWindowMonitor windowMonitor) { + this.displayMonitor = displayMonitor; this.logger = logger; this.processMonitor = processMonitor; this.taskbar = taskbar; this.windowMonitor = windowMonitor; - this.workingArea = workingArea; } public void Start() { + displayMonitor.DisplayChanged += DisplayMonitor_DisplaySettingsChanged; processMonitor.ExplorerStarted += ProcessMonitor_ExplorerStarted; windowMonitor.WindowChanged += WindowMonitor_WindowChanged; } @@ -49,12 +49,21 @@ namespace SafeExamBrowser.Core.Behaviour windowMonitor.WindowChanged -= WindowMonitor_WindowChanged; } + private void DisplayMonitor_DisplaySettingsChanged() + { + logger.Info("Reinitializing working area..."); + displayMonitor.InitializePrimaryDisplay(taskbar.GetAbsoluteHeight()); + logger.Info("Reinitializing taskbar bounds..."); + taskbar.InitializeBounds(); + logger.Info("Desktop successfully restored."); + } + private void ProcessMonitor_ExplorerStarted() { logger.Info("Trying to shut down explorer..."); processMonitor.CloseExplorerShell(); logger.Info("Reinitializing working area..."); - workingArea.InitializeFor(taskbar); + displayMonitor.InitializePrimaryDisplay(taskbar.GetAbsoluteHeight()); logger.Info("Reinitializing taskbar bounds..."); taskbar.InitializeBounds(); logger.Info("Desktop successfully restored."); diff --git a/SafeExamBrowser.Core/SafeExamBrowser.Core.csproj b/SafeExamBrowser.Core/SafeExamBrowser.Core.csproj index 1bb728e2..b564b361 100644 --- a/SafeExamBrowser.Core/SafeExamBrowser.Core.csproj +++ b/SafeExamBrowser.Core/SafeExamBrowser.Core.csproj @@ -67,7 +67,7 @@ - + diff --git a/SafeExamBrowser.Configuration/Bounds.cs b/SafeExamBrowser.Monitoring/Display/Bounds.cs similarity index 91% rename from SafeExamBrowser.Configuration/Bounds.cs rename to SafeExamBrowser.Monitoring/Display/Bounds.cs index ce83b546..62237bae 100644 --- a/SafeExamBrowser.Configuration/Bounds.cs +++ b/SafeExamBrowser.Monitoring/Display/Bounds.cs @@ -8,7 +8,7 @@ using SafeExamBrowser.Contracts.WindowsApi; -namespace SafeExamBrowser.Configuration +namespace SafeExamBrowser.Monitoring.Display { internal class Bounds : IBounds { diff --git a/SafeExamBrowser.Monitoring/Display/DisplayMonitor.cs b/SafeExamBrowser.Monitoring/Display/DisplayMonitor.cs new file mode 100644 index 00000000..8f88a11e --- /dev/null +++ b/SafeExamBrowser.Monitoring/Display/DisplayMonitor.cs @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2017 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +using System; +using System.Windows.Forms; +using Microsoft.Win32; +using SafeExamBrowser.Contracts.Logging; +using SafeExamBrowser.Contracts.Monitoring; +using SafeExamBrowser.Contracts.WindowsApi; + +namespace SafeExamBrowser.Monitoring.Display +{ + public class DisplayMonitor : IDisplayMonitor + { + private IBounds originalWorkingArea; + private ILogger logger; + private INativeMethods nativeMethods; + private string wallpaper; + + public event DisplayChangedEventHandler DisplayChanged; + + public DisplayMonitor(ILogger logger, INativeMethods nativeMethods) + { + this.logger = logger; + this.nativeMethods = nativeMethods; + } + + public void InitializePrimaryDisplay(int taskbarHeight) + { + InitializeWorkingArea(taskbarHeight); + InitializeWallpaper(); + PreventSleepMode(); + } + + public void ResetPrimaryDisplay() + { + ResetWorkingArea(); + ResetWallpaper(); + } + + public void StartMonitoringDisplayChanges() + { + SystemEvents.DisplaySettingsChanged += SystemEvents_DisplaySettingsChanged; + logger.Info("Started monitoring display changes."); + } + + public void StopMonitoringDisplayChanges() + { + SystemEvents.DisplaySettingsChanged -= SystemEvents_DisplaySettingsChanged; + logger.Info("Stopped monitoring display changes."); + } + + private void SystemEvents_DisplaySettingsChanged(object sender, EventArgs e) + { + logger.Info("Display change detected!"); + DisplayChanged?.Invoke(); + } + + private void InitializeWorkingArea(int taskbarHeight) + { + var identifier = GetIdentifierForPrimaryDisplay(); + + originalWorkingArea = nativeMethods.GetWorkingArea(); + LogWorkingArea($"Saved original working area for {identifier}", originalWorkingArea); + + var area = new Bounds + { + Left = 0, + Top = 0, + Right = Screen.PrimaryScreen.Bounds.Width, + Bottom = Screen.PrimaryScreen.Bounds.Height - taskbarHeight + }; + + LogWorkingArea($"Trying to set new working area for {identifier}", area); + nativeMethods.SetWorkingArea(area); + LogWorkingArea($"Working area of {identifier} is now set to", nativeMethods.GetWorkingArea()); + } + + private void InitializeWallpaper() + { + var path = nativeMethods.GetWallpaperPath(); + + if (!String.IsNullOrEmpty(path)) + { + wallpaper = path; + logger.Info($"Saved wallpaper image: {wallpaper}."); + nativeMethods.RemoveWallpaper(); + logger.Info("Removed current wallpaper."); + } + } + + private void PreventSleepMode() + { + nativeMethods.DisableSleep(); + logger.Info("Disabled sleep mode and display timeout."); + } + + private void ResetWorkingArea() + { + var identifier = GetIdentifierForPrimaryDisplay(); + + if (originalWorkingArea != null) + { + nativeMethods.SetWorkingArea(originalWorkingArea); + LogWorkingArea($"Restored original working area for {identifier}", originalWorkingArea); + } + else + { + logger.Warn($"Could not restore original working area for {identifier}!"); + } + } + + private void ResetWallpaper() + { + if (!String.IsNullOrEmpty(wallpaper)) + { + nativeMethods.SetWallpaper(wallpaper); + logger.Info($"Restored wallpaper image: {wallpaper}."); + } + } + + private string GetIdentifierForPrimaryDisplay() + { + var display = Screen.PrimaryScreen.DeviceName?.Replace(@"\\.\", string.Empty); + + return $"{display} ({Screen.PrimaryScreen.Bounds.Width}x{Screen.PrimaryScreen.Bounds.Height})"; + } + + private void LogWorkingArea(string message, IBounds area) + { + logger.Info($"{message}: Left = {area.Left}, Top = {area.Top}, Right = {area.Right}, Bottom = {area.Bottom}."); + } + } +} diff --git a/SafeExamBrowser.Monitoring/SafeExamBrowser.Monitoring.csproj b/SafeExamBrowser.Monitoring/SafeExamBrowser.Monitoring.csproj index 6f4187fe..881c82ae 100644 --- a/SafeExamBrowser.Monitoring/SafeExamBrowser.Monitoring.csproj +++ b/SafeExamBrowser.Monitoring/SafeExamBrowser.Monitoring.csproj @@ -50,7 +50,9 @@ + + @@ -60,10 +62,13 @@ + + + diff --git a/SafeExamBrowser.Monitoring/Windows/Window.cs b/SafeExamBrowser.Monitoring/Windows/Window.cs new file mode 100644 index 00000000..27ea0cba --- /dev/null +++ b/SafeExamBrowser.Monitoring/Windows/Window.cs @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2017 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +using System; + +namespace SafeExamBrowser.Monitoring.Windows +{ + internal struct Window + { + internal IntPtr Handle { get; set; } + internal string Title { get; set; } + } +} diff --git a/SafeExamBrowser.Monitoring/Windows/WindowMonitor.cs b/SafeExamBrowser.Monitoring/Windows/WindowMonitor.cs index 84256968..73fe64f8 100644 --- a/SafeExamBrowser.Monitoring/Windows/WindowMonitor.cs +++ b/SafeExamBrowser.Monitoring/Windows/WindowMonitor.cs @@ -117,11 +117,5 @@ namespace SafeExamBrowser.Monitoring.Windows { WindowChanged?.Invoke(window); } - - private struct Window - { - internal IntPtr Handle { get; set; } - internal string Title { get; set; } - } } } diff --git a/SafeExamBrowser.UserInterface/Taskbar.xaml.cs b/SafeExamBrowser.UserInterface/Taskbar.xaml.cs index 6a6af27f..e8d04c38 100644 --- a/SafeExamBrowser.UserInterface/Taskbar.xaml.cs +++ b/SafeExamBrowser.UserInterface/Taskbar.xaml.cs @@ -10,14 +10,19 @@ using System.Windows; using System.Windows.Input; using System.Windows.Interop; using System.Windows.Media; +using SafeExamBrowser.Contracts.Logging; using SafeExamBrowser.Contracts.UserInterface; namespace SafeExamBrowser.UserInterface { public partial class Taskbar : Window, ITaskbar { - public Taskbar() + private ILogger logger; + + public Taskbar(ILogger logger) { + this.logger = logger; + InitializeComponent(); Loaded += (o, args) => InitializeBounds(); @@ -41,28 +46,13 @@ namespace SafeExamBrowser.UserInterface public int GetAbsoluteHeight() { - // WPF works with device-independent pixels. The following code is required - // to get the real height of the taskbar (in absolute, device-specific pixels). - // Source: https://stackoverflow.com/questions/3286175/how-do-i-convert-a-wpf-size-to-physical-pixels - return Dispatcher.Invoke(() => { - Matrix transformToDevice; - var source = PresentationSource.FromVisual(this); + var height = (int) TransformToPhysical(Width, Height).Y; - if (source != null) - { - transformToDevice = source.CompositionTarget.TransformToDevice; - } - else - { - using (var newSource = new HwndSource(new HwndSourceParameters())) - { - transformToDevice = newSource.CompositionTarget.TransformToDevice; - } - } + logger.Info($"Calculated absolute taskbar height is {height}px."); - return (int)transformToDevice.Transform((Vector)new Size(Width, Height)).Y; + return height; }); } @@ -73,6 +63,11 @@ namespace SafeExamBrowser.UserInterface Width = SystemParameters.WorkArea.Right; Left = SystemParameters.WorkArea.Right - Width; Top = SystemParameters.WorkArea.Bottom; + + var position = TransformToPhysical(Left, Top); + var size = TransformToPhysical(Width, Height); + + logger.Info($"Set taskbar bounds to {Width}x{Height} at ({Left}/{Top}), in physical pixels: {size.X}x{size.Y} at ({position.X}/{position.Y})."); }); } @@ -106,5 +101,29 @@ namespace SafeExamBrowser.UserInterface } } } + + private Vector TransformToPhysical(double x, double y) + { + // WPF works with device-independent pixels. The following code is required + // to transform those values to their absolute, device-specific pixel value. + // Source: https://stackoverflow.com/questions/3286175/how-do-i-convert-a-wpf-size-to-physical-pixels + + Matrix transformToDevice; + var source = PresentationSource.FromVisual(this); + + if (source != null) + { + transformToDevice = source.CompositionTarget.TransformToDevice; + } + else + { + using (var newSource = new HwndSource(new HwndSourceParameters())) + { + transformToDevice = newSource.CompositionTarget.TransformToDevice; + } + } + + return transformToDevice.Transform(new Vector(x, y)); + } } } diff --git a/SafeExamBrowser.WindowsApi/Constants/SPI.cs b/SafeExamBrowser.WindowsApi/Constants/SPI.cs index a64bd0d1..319f865a 100644 --- a/SafeExamBrowser.WindowsApi/Constants/SPI.cs +++ b/SafeExamBrowser.WindowsApi/Constants/SPI.cs @@ -14,12 +14,11 @@ namespace SafeExamBrowser.WindowsApi.Constants internal enum SPI : uint { /// - /// Sets the size of the work area. The work area is the portion of the screen not obscured by the system taskbar - /// or by application desktop toolbars. The pvParam parameter is a pointer to a RECT structure that specifies the - /// new work area rectangle, expressed in virtual screen coordinates. In a system with multiple display monitors, - /// the function sets the work area of the monitor that contains the specified rectangle. + /// Retrieves the full path of the bitmap file for the desktop wallpaper. The pvParam parameter must point to a buffer + /// that receives a null-terminated path string. Set the uiParam parameter to the size, in characters, of the pvParam buffer. + /// The returned string will not exceed MAX_PATH characters. If there is no desktop wallpaper, the returned string is empty. /// - SETWORKAREA = 0x002F, + GETDESKWALLPAPER = 0x73, /// /// Retrieves the size of the work area on the primary display monitor. The work area is the portion of the screen @@ -27,6 +26,21 @@ namespace SafeExamBrowser.WindowsApi.Constants /// RECT structure that receives the coordinates of the work area, expressed in virtual screen coordinates. To get /// the work area of a monitor other than the primary display monitor, call the GetMonitorInfo function. /// - GETWORKAREA = 0x0030, + GETWORKAREA = 0x30, + + /// + /// Sets the desktop wallpaper. The value of the pvParam parameter determines the new wallpaper. To specify a wallpaper bitmap, + /// set pvParam to point to a null-terminated string containing the name of a bitmap file. Setting pvParam to "" removes the + /// wallpaper. Setting pvParam to SETWALLPAPER_DEFAULT or null reverts to the default wallpaper. + /// + SETDESKWALLPAPER = 0x14, + + /// + /// Sets the size of the work area. The work area is the portion of the screen not obscured by the system taskbar + /// or by application desktop toolbars. The pvParam parameter is a pointer to a RECT structure that specifies the + /// new work area rectangle, expressed in virtual screen coordinates. In a system with multiple display monitors, + /// the function sets the work area of the monitor that contains the specified rectangle. + /// + SETWORKAREA = 0x2F, } } diff --git a/SafeExamBrowser.WindowsApi/Kernel32.cs b/SafeExamBrowser.WindowsApi/Kernel32.cs index 6fc2ec4c..1c092559 100644 --- a/SafeExamBrowser.WindowsApi/Kernel32.cs +++ b/SafeExamBrowser.WindowsApi/Kernel32.cs @@ -8,6 +8,7 @@ using System; using System.Runtime.InteropServices; +using SafeExamBrowser.WindowsApi.Types; namespace SafeExamBrowser.WindowsApi { @@ -16,7 +17,10 @@ namespace SafeExamBrowser.WindowsApi /// internal class Kernel32 { - [DllImport("kernel32.dll", CharSet = CharSet.Auto)] - public static extern IntPtr GetModuleHandle(string lpModuleName); + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + internal static extern IntPtr GetModuleHandle(string lpModuleName); + + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern EXECUTION_STATE SetThreadExecutionState(EXECUTION_STATE esFlags); } } diff --git a/SafeExamBrowser.WindowsApi/NativeMethods.cs b/SafeExamBrowser.WindowsApi/NativeMethods.cs index fbaf5d19..a5b03fd2 100644 --- a/SafeExamBrowser.WindowsApi/NativeMethods.cs +++ b/SafeExamBrowser.WindowsApi/NativeMethods.cs @@ -94,6 +94,11 @@ namespace SafeExamBrowser.WindowsApi EventDelegates.TryRemove(handle, out EventProc d); } + public void DisableSleep() + { + Kernel32.SetThreadExecutionState(EXECUTION_STATE.CONTINUOUS | EXECUTION_STATE.DISPLAY_REQUIRED | EXECUTION_STATE.SYSTEM_REQUIRED); + } + public void EmptyClipboard() { var success = true; @@ -149,6 +154,22 @@ namespace SafeExamBrowser.WindowsApi return processId; } + public string GetWallpaperPath() + { + const int MAX_PATH = 260; + var buffer = new String('\0', MAX_PATH); + var success = User32.SystemParametersInfo(SPI.GETDESKWALLPAPER, buffer.Length, buffer, 0); + + if (!success) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + var path = buffer.Substring(0, buffer.IndexOf('\0')); + + return path; + } + public string GetWindowTitle(IntPtr window) { var length = User32.GetWindowTextLength(window); @@ -256,6 +277,11 @@ namespace SafeExamBrowser.WindowsApi return handle; } + public void RemoveWallpaper() + { + SetWallpaper(string.Empty); + } + public void RestoreWindow(IntPtr window) { User32.ShowWindow(window, (int)ShowWindowCommand.Restore); @@ -266,6 +292,16 @@ namespace SafeExamBrowser.WindowsApi User32.SendMessage(window, Constant.WM_SYSCOMMAND, (IntPtr) SystemCommand.CLOSE, IntPtr.Zero); } + public void SetWallpaper(string filePath) + { + var success = User32.SystemParametersInfo(SPI.SETDESKWALLPAPER, 0, filePath, SPIF.UPDATEANDCHANGE); + + if (!success) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + } + public void SetWorkingArea(IBounds bounds) { var workingArea = new RECT { Left = bounds.Left, Top = bounds.Top, Right = bounds.Right, Bottom = bounds.Bottom }; diff --git a/SafeExamBrowser.WindowsApi/SafeExamBrowser.WindowsApi.csproj b/SafeExamBrowser.WindowsApi/SafeExamBrowser.WindowsApi.csproj index 900b809a..4bd2b3db 100644 --- a/SafeExamBrowser.WindowsApi/SafeExamBrowser.WindowsApi.csproj +++ b/SafeExamBrowser.WindowsApi/SafeExamBrowser.WindowsApi.csproj @@ -62,6 +62,7 @@ + diff --git a/SafeExamBrowser.WindowsApi/Types/EXECUTION_STATE.cs b/SafeExamBrowser.WindowsApi/Types/EXECUTION_STATE.cs new file mode 100644 index 00000000..4d1ea39c --- /dev/null +++ b/SafeExamBrowser.WindowsApi/Types/EXECUTION_STATE.cs @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2017 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +using System; + +namespace SafeExamBrowser.WindowsApi.Types +{ + /// + /// See http://www.pinvoke.net/default.aspx/kernel32/SetThreadExecutionState.html. + /// See https://msdn.microsoft.com/en-us/library/aa373208(v=vs.85).aspx. + /// + [Flags] + public enum EXECUTION_STATE : uint + { + AWAYMODE_REQUIRED = 0x00000040, + CONTINUOUS = 0x80000000, + DISPLAY_REQUIRED = 0x00000002, + SYSTEM_REQUIRED = 0x00000001 + } +} diff --git a/SafeExamBrowser.WindowsApi/Types/RECT.cs b/SafeExamBrowser.WindowsApi/Types/RECT.cs index c520e253..8e9ac824 100644 --- a/SafeExamBrowser.WindowsApi/Types/RECT.cs +++ b/SafeExamBrowser.WindowsApi/Types/RECT.cs @@ -11,9 +11,9 @@ using SafeExamBrowser.Contracts.WindowsApi; namespace SafeExamBrowser.WindowsApi.Types { - /// + /// /// See https://msdn.microsoft.com/en-us/library/windows/desktop/dd162897(v=vs.85).aspx. - /// + /// [StructLayout(LayoutKind.Sequential)] internal struct RECT { diff --git a/SafeExamBrowser.WindowsApi/User32.cs b/SafeExamBrowser.WindowsApi/User32.cs index f888d4f3..e31b8e41 100644 --- a/SafeExamBrowser.WindowsApi/User32.cs +++ b/SafeExamBrowser.WindowsApi/User32.cs @@ -38,10 +38,10 @@ namespace SafeExamBrowser.WindowsApi [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool EnumWindows(EnumWindowsDelegate enumProc, IntPtr lParam); - [DllImport("user32.dll", SetLastError = true)] + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] internal static extern IntPtr FindWindow(string lpClassName, string lpWindowName); - [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] internal static extern int GetWindowText(IntPtr hWnd, StringBuilder strText, int maxCount); [DllImport("user32.dll", SetLastError = true)] @@ -79,6 +79,10 @@ namespace SafeExamBrowser.WindowsApi [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool SystemParametersInfo(SPI uiAction, uint uiParam, ref RECT pvParam, SPIF fWinIni); + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool SystemParametersInfo(SPI uiAction, int uiParam, string pvParam, SPIF fWinIni); + [DllImport("user32.dll", SetLastError = true)] internal static extern bool UnhookWinEvent(IntPtr hWinEventHook); diff --git a/SafeExamBrowser/CompositionRoot.cs b/SafeExamBrowser/CompositionRoot.cs index 5403d6b9..4122cb8e 100644 --- a/SafeExamBrowser/CompositionRoot.cs +++ b/SafeExamBrowser/CompositionRoot.cs @@ -21,6 +21,7 @@ using SafeExamBrowser.Core.Behaviour; using SafeExamBrowser.Core.Behaviour.Operations; using SafeExamBrowser.Core.I18n; using SafeExamBrowser.Core.Logging; +using SafeExamBrowser.Monitoring.Display; using SafeExamBrowser.Monitoring.Keyboard; using SafeExamBrowser.Monitoring.Mouse; using SafeExamBrowser.Monitoring.Processes; @@ -34,6 +35,7 @@ namespace SafeExamBrowser { private IApplicationController browserController; private IApplicationInfo browserInfo; + private IDisplayMonitor displayMonitor; private IKeyboardInterceptor keyboardInterceptor; private ILogger logger; private ILogContentFormatter logFormatter; @@ -46,7 +48,6 @@ namespace SafeExamBrowser private ITextResource textResource; private IUserInterfaceFactory uiFactory; private IWindowMonitor windowMonitor; - private IWorkingArea workingArea; public IShutdownController ShutdownController { get; private set; } public IStartupController StartupController { get; private set; } @@ -59,22 +60,22 @@ namespace SafeExamBrowser logger = new Logger(); logFormatter = new DefaultLogFormatter(); nativeMethods = new NativeMethods(); - settings = new SettingsImpl(); - Taskbar = new Taskbar(); + settings = new Settings(); textResource = new XmlTextResource(); uiFactory = new UserInterfaceFactory(); logger.Subscribe(new LogFileWriter(logFormatter, settings)); text = new Text(textResource); + Taskbar = new Taskbar(new ModuleLogger(logger, typeof(Taskbar))); browserController = new BrowserApplicationController(settings, text, uiFactory); + displayMonitor = new DisplayMonitor(new ModuleLogger(logger, typeof(DisplayMonitor)), nativeMethods); keyboardInterceptor = new KeyboardInterceptor(settings.Keyboard, new ModuleLogger(logger, typeof(KeyboardInterceptor))); mouseInterceptor = new MouseInterceptor(new ModuleLogger(logger, typeof(MouseInterceptor)), settings.Mouse); processMonitor = new ProcessMonitor(new ModuleLogger(logger, typeof(ProcessMonitor)), nativeMethods); windowMonitor = new WindowMonitor(new ModuleLogger(logger, typeof(WindowMonitor)), nativeMethods); - workingArea = new WorkingArea(new ModuleLogger(logger, typeof(WorkingArea)), nativeMethods); - runtimeController = new RuntimeController(new ModuleLogger(logger, typeof(RuntimeController)), processMonitor, Taskbar, windowMonitor, workingArea); + runtimeController = new RuntimeController(displayMonitor, new ModuleLogger(logger, typeof(RuntimeController)), processMonitor, Taskbar, windowMonitor); ShutdownController = new ShutdownController(logger, settings, text, uiFactory); StartupController = new StartupController(logger, settings, text, uiFactory); @@ -82,7 +83,7 @@ namespace SafeExamBrowser StartupOperations.Enqueue(new KeyboardInterceptorOperation(keyboardInterceptor, logger, nativeMethods)); StartupOperations.Enqueue(new WindowMonitorOperation(logger, windowMonitor)); StartupOperations.Enqueue(new ProcessMonitorOperation(logger, processMonitor)); - StartupOperations.Enqueue(new WorkingAreaOperation(logger, Taskbar, workingArea)); + StartupOperations.Enqueue(new DisplayMonitorOperation(displayMonitor, logger, Taskbar)); StartupOperations.Enqueue(new TaskbarOperation(logger, logFormatter, settings, Taskbar, text, uiFactory)); StartupOperations.Enqueue(new BrowserOperation(browserController, browserInfo, logger, Taskbar, uiFactory)); StartupOperations.Enqueue(new RuntimeControllerOperation(runtimeController, logger));