From 8a3039ec1638e76b4abdcae6340e2cbe6e3c78ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damian=20B=C3=BCchel?= Date: Fri, 17 Sep 2021 10:47:02 +0200 Subject: [PATCH] SEBWIN-516: Implemented raise hand feature. --- .../Handlers/ResourceHandlerTests.cs | 2 +- .../Operations/ProctoringOperation.cs | 7 + .../ConfigurationData/DataProcessorTests.cs | 4 + .../ConfigurationData/DataValues.cs | 2 + SafeExamBrowser.I18n.Contracts/TextKey.cs | 4 + SafeExamBrowser.I18n/Data/de.xml | 12 ++ SafeExamBrowser.I18n/Data/en.xml | 12 ++ SafeExamBrowser.I18n/Data/fr.xml | 12 ++ SafeExamBrowser.I18n/Data/it.xml | 12 ++ SafeExamBrowser.I18n/Data/zh.xml | 12 ++ .../Events/ProctoringEventHandler.cs | 15 +++ .../IProctoringController.cs | 26 ++++ ...afeExamBrowser.Proctoring.Contracts.csproj | 1 + .../ProctoringController.cs | 45 +++++++ .../Events/ServerEventHandler.cs | 15 +++ .../IServerProxy.cs | 21 ++- .../SafeExamBrowser.Server.Contracts.csproj | 1 + SafeExamBrowser.Server/Data/Attributes.cs | 2 + SafeExamBrowser.Server/Data/Instructions.cs | 1 + SafeExamBrowser.Server/Parser.cs | 16 +++ SafeExamBrowser.Server/ServerProxy.cs | 58 ++++++++ .../Proctoring/ProctoringSettings.cs | 10 ++ .../IUserInterfaceFactory.cs | 7 + ...ExamBrowser.UserInterface.Contracts.csproj | 4 + .../ActionCenter/RaiseHandControl.xaml | 37 ++++++ .../ActionCenter/RaiseHandControl.xaml.cs | 116 ++++++++++++++++ .../Controls/Taskbar/RaiseHandControl.xaml | 38 ++++++ .../Controls/Taskbar/RaiseHandControl.xaml.cs | 125 ++++++++++++++++++ ...feExamBrowser.UserInterface.Desktop.csproj | 18 +++ .../UserInterfaceFactory.cs | 14 ++ .../ActionCenter/RaiseHandControl.xaml | 37 ++++++ .../ActionCenter/RaiseHandControl.xaml.cs | 116 ++++++++++++++++ .../Controls/Taskbar/RaiseHandControl.xaml | 37 ++++++ .../Controls/Taskbar/RaiseHandControl.xaml.cs | 125 ++++++++++++++++++ ...afeExamBrowser.UserInterface.Mobile.csproj | 18 +++ .../UserInterfaceFactory.cs | 14 ++ 36 files changed, 992 insertions(+), 4 deletions(-) create mode 100644 SafeExamBrowser.Proctoring.Contracts/Events/ProctoringEventHandler.cs create mode 100644 SafeExamBrowser.Server.Contracts/Events/ServerEventHandler.cs create mode 100644 SafeExamBrowser.UserInterface.Desktop/Controls/ActionCenter/RaiseHandControl.xaml create mode 100644 SafeExamBrowser.UserInterface.Desktop/Controls/ActionCenter/RaiseHandControl.xaml.cs create mode 100644 SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/RaiseHandControl.xaml create mode 100644 SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/RaiseHandControl.xaml.cs create mode 100644 SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenter/RaiseHandControl.xaml create mode 100644 SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenter/RaiseHandControl.xaml.cs create mode 100644 SafeExamBrowser.UserInterface.Mobile/Controls/Taskbar/RaiseHandControl.xaml create mode 100644 SafeExamBrowser.UserInterface.Mobile/Controls/Taskbar/RaiseHandControl.xaml.cs diff --git a/SafeExamBrowser.Browser.UnitTests/Handlers/ResourceHandlerTests.cs b/SafeExamBrowser.Browser.UnitTests/Handlers/ResourceHandlerTests.cs index 1665bee0..f0931284 100644 --- a/SafeExamBrowser.Browser.UnitTests/Handlers/ResourceHandlerTests.cs +++ b/SafeExamBrowser.Browser.UnitTests/Handlers/ResourceHandlerTests.cs @@ -114,7 +114,7 @@ namespace SafeExamBrowser.Browser.UnitTests.Handlers public void MustFilterContentRequests() { var request = new Mock(); - var url = "www.test.org"; + var url = "http://www.test.org"; filter.Setup(f => f.Process(It.Is(r => r.Url.Equals(url)))).Returns(FilterResult.Block); request.SetupGet(r => r.ResourceType).Returns(ResourceType.SubFrame); diff --git a/SafeExamBrowser.Client/Operations/ProctoringOperation.cs b/SafeExamBrowser.Client/Operations/ProctoringOperation.cs index 2de795ba..e5e9b7a7 100644 --- a/SafeExamBrowser.Client/Operations/ProctoringOperation.cs +++ b/SafeExamBrowser.Client/Operations/ProctoringOperation.cs @@ -12,6 +12,7 @@ using SafeExamBrowser.Core.Contracts.OperationModel.Events; using SafeExamBrowser.I18n.Contracts; using SafeExamBrowser.Logging.Contracts; using SafeExamBrowser.Proctoring.Contracts; +using SafeExamBrowser.Settings; using SafeExamBrowser.UserInterface.Contracts; using SafeExamBrowser.UserInterface.Contracts.Shell; @@ -56,6 +57,12 @@ namespace SafeExamBrowser.Client.Operations controller.Initialize(Context.Settings.Proctoring); actionCenter.AddNotificationControl(uiFactory.CreateNotificationControl(notification, Location.ActionCenter)); + if (Context.Settings.SessionMode == SessionMode.Server && Context.Settings.Proctoring.ShowRaiseHandNotification) + { + actionCenter.AddNotificationControl(uiFactory.CreateRaiseHandControl(controller, Location.ActionCenter, Context.Settings.Proctoring)); + taskbar.AddNotificationControl(uiFactory.CreateRaiseHandControl(controller, Location.Taskbar, Context.Settings.Proctoring)); + } + if (Context.Settings.Proctoring.ShowTaskbarNotification) { taskbar.AddNotificationControl(uiFactory.CreateNotificationControl(notification, Location.Taskbar)); diff --git a/SafeExamBrowser.Configuration.UnitTests/ConfigurationData/DataProcessorTests.cs b/SafeExamBrowser.Configuration.UnitTests/ConfigurationData/DataProcessorTests.cs index 7469c53a..84a3642f 100644 --- a/SafeExamBrowser.Configuration.UnitTests/ConfigurationData/DataProcessorTests.cs +++ b/SafeExamBrowser.Configuration.UnitTests/ConfigurationData/DataProcessorTests.cs @@ -36,7 +36,9 @@ namespace SafeExamBrowser.Configuration.UnitTests.ConfigurationData raw.Add(Keys.Browser.ShowReloadButton, true); settings.Browser.AdditionalWindow.AllowReloading = false; + settings.Browser.AdditionalWindow.ShowReloadButton = false; settings.Browser.MainWindow.AllowReloading = false; + settings.Browser.MainWindow.ShowReloadButton = false; sut.Process(raw, settings); @@ -44,7 +46,9 @@ namespace SafeExamBrowser.Configuration.UnitTests.ConfigurationData Assert.IsFalse(settings.Browser.MainWindow.ShowToolbar); settings.Browser.AdditionalWindow.AllowReloading = true; + settings.Browser.AdditionalWindow.ShowReloadButton = true; settings.Browser.MainWindow.AllowReloading = true; + settings.Browser.MainWindow.ShowReloadButton = true; sut.Process(raw, settings); diff --git a/SafeExamBrowser.Configuration/ConfigurationData/DataValues.cs b/SafeExamBrowser.Configuration/ConfigurationData/DataValues.cs index 2cc080da..3d1fecde 100644 --- a/SafeExamBrowser.Configuration/ConfigurationData/DataValues.cs +++ b/SafeExamBrowser.Configuration/ConfigurationData/DataValues.cs @@ -219,6 +219,7 @@ namespace SafeExamBrowser.Configuration.ConfigurationData settings.Mouse.AllowRightButton = true; settings.Proctoring.Enabled = false; + settings.Proctoring.ForceRaiseHandMessage = false; settings.Proctoring.JitsiMeet.AllowChat = false; settings.Proctoring.JitsiMeet.AllowClosedCaptions = false; settings.Proctoring.JitsiMeet.AllowRaiseHand = false; @@ -233,6 +234,7 @@ namespace SafeExamBrowser.Configuration.ConfigurationData settings.Proctoring.JitsiMeet.SendVideo = true; settings.Proctoring.JitsiMeet.ShowMeetingName = false; settings.Proctoring.JitsiMeet.VideoMuted = false; + settings.Proctoring.ShowRaiseHandNotification = true; settings.Proctoring.ShowTaskbarNotification = true; settings.Proctoring.WindowVisibility = WindowVisibility.Hidden; settings.Proctoring.Zoom.AllowChat = false; diff --git a/SafeExamBrowser.I18n.Contracts/TextKey.cs b/SafeExamBrowser.I18n.Contracts/TextKey.cs index 36eb48cd..2bb645f6 100644 --- a/SafeExamBrowser.I18n.Contracts/TextKey.cs +++ b/SafeExamBrowser.I18n.Contracts/TextKey.cs @@ -137,7 +137,11 @@ namespace SafeExamBrowser.I18n.Contracts Notification_AboutTooltip, Notification_LogTooltip, Notification_ProctoringActiveTooltip, + Notification_ProctoringHandLowered, + Notification_ProctoringHandRaised, Notification_ProctoringInactiveTooltip, + Notification_ProctoringLowerHand, + Notification_ProctoringRaiseHand, OperationStatus_CloseRuntimeConnection, OperationStatus_EmptyClipboard, OperationStatus_FinalizeApplications, diff --git a/SafeExamBrowser.I18n/Data/de.xml b/SafeExamBrowser.I18n/Data/de.xml index 3db4478f..7057dfaa 100644 --- a/SafeExamBrowser.I18n/Data/de.xml +++ b/SafeExamBrowser.I18n/Data/de.xml @@ -369,9 +369,21 @@ Fernüberwachung ist aktiv + + Hand ist gesenkt + + + Hand ist erhoben + Fernüberwachung ist nicht aktiv + + Hand senken + + + Hand erheben + Schliesse Verbindung zur Runtime diff --git a/SafeExamBrowser.I18n/Data/en.xml b/SafeExamBrowser.I18n/Data/en.xml index 2d0302ed..1df9527a 100644 --- a/SafeExamBrowser.I18n/Data/en.xml +++ b/SafeExamBrowser.I18n/Data/en.xml @@ -369,9 +369,21 @@ Remote proctoring is active + + Hand is lowered + + + Hand is raised + Remote proctoring is inactive + + Lower hand + + + Raise hand + Closing runtime connection diff --git a/SafeExamBrowser.I18n/Data/fr.xml b/SafeExamBrowser.I18n/Data/fr.xml index 31574f09..2b69e00f 100644 --- a/SafeExamBrowser.I18n/Data/fr.xml +++ b/SafeExamBrowser.I18n/Data/fr.xml @@ -369,9 +369,21 @@ La surveillance à distance est active + + La main est baissée + + + La main est levée + La surveillance à distance est inactive + + Baisser la main + + + Lever la main + Fermeture de la connexion diff --git a/SafeExamBrowser.I18n/Data/it.xml b/SafeExamBrowser.I18n/Data/it.xml index bf80f846..bf42600b 100644 --- a/SafeExamBrowser.I18n/Data/it.xml +++ b/SafeExamBrowser.I18n/Data/it.xml @@ -369,9 +369,21 @@ Il proctoring remoto è attivo + + La mano è abbassata + + + La mano è alzata + Il proctoring remoto è inattivo + + Abbassa la mano + + + Alzi la mano + Chiusura della connessione runtime diff --git a/SafeExamBrowser.I18n/Data/zh.xml b/SafeExamBrowser.I18n/Data/zh.xml index a8d9492a..c870b338 100644 --- a/SafeExamBrowser.I18n/Data/zh.xml +++ b/SafeExamBrowser.I18n/Data/zh.xml @@ -333,9 +333,21 @@ 远程监理处于活动状态 + + 手被放下 + + + 举手了 + 远程监理处于不活动状态 + + 把手放低 + + + 举手 + 关闭运行时连接 diff --git a/SafeExamBrowser.Proctoring.Contracts/Events/ProctoringEventHandler.cs b/SafeExamBrowser.Proctoring.Contracts/Events/ProctoringEventHandler.cs new file mode 100644 index 00000000..c6c8153d --- /dev/null +++ b/SafeExamBrowser.Proctoring.Contracts/Events/ProctoringEventHandler.cs @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2021 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.Proctoring.Contracts.Events +{ + /// + /// The default event handler for proctoring events. + /// + public delegate void ProctoringEventHandler(); +} diff --git a/SafeExamBrowser.Proctoring.Contracts/IProctoringController.cs b/SafeExamBrowser.Proctoring.Contracts/IProctoringController.cs index e4bfc6bc..8ad45934 100644 --- a/SafeExamBrowser.Proctoring.Contracts/IProctoringController.cs +++ b/SafeExamBrowser.Proctoring.Contracts/IProctoringController.cs @@ -6,6 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +using SafeExamBrowser.Proctoring.Contracts.Events; using SafeExamBrowser.Settings.Proctoring; namespace SafeExamBrowser.Proctoring.Contracts @@ -15,11 +16,36 @@ namespace SafeExamBrowser.Proctoring.Contracts /// public interface IProctoringController { + /// + /// Indicates whether the hand is currently raised. + /// + bool IsHandRaised { get; } + + /// + /// Fired when the hand has been lowered. + /// + event ProctoringEventHandler HandLowered; + + /// + /// Fired when the hand has been raised. + /// + event ProctoringEventHandler HandRaised; + /// /// Initializes the given settings and starts the proctoring if the settings are valid. /// void Initialize(ProctoringSettings settings); + /// + /// Lowers the hand. + /// + void LowerHand(); + + /// + /// Raises the hand, optionally with the given message. + /// + void RaiseHand(string message = default(string)); + /// /// Stops the proctoring functionality. /// diff --git a/SafeExamBrowser.Proctoring.Contracts/SafeExamBrowser.Proctoring.Contracts.csproj b/SafeExamBrowser.Proctoring.Contracts/SafeExamBrowser.Proctoring.Contracts.csproj index f698b899..c3fdc881 100644 --- a/SafeExamBrowser.Proctoring.Contracts/SafeExamBrowser.Proctoring.Contracts.csproj +++ b/SafeExamBrowser.Proctoring.Contracts/SafeExamBrowser.Proctoring.Contracts.csproj @@ -54,6 +54,7 @@ + diff --git a/SafeExamBrowser.Proctoring/ProctoringController.cs b/SafeExamBrowser.Proctoring/ProctoringController.cs index 1842cf47..b6de096f 100644 --- a/SafeExamBrowser.Proctoring/ProctoringController.cs +++ b/SafeExamBrowser.Proctoring/ProctoringController.cs @@ -19,6 +19,7 @@ using SafeExamBrowser.Core.Contracts.Resources.Icons; using SafeExamBrowser.I18n.Contracts; using SafeExamBrowser.Logging.Contracts; using SafeExamBrowser.Proctoring.Contracts; +using SafeExamBrowser.Proctoring.Contracts.Events; using SafeExamBrowser.Server.Contracts; using SafeExamBrowser.Server.Contracts.Events; using SafeExamBrowser.Settings.Proctoring; @@ -44,8 +45,11 @@ namespace SafeExamBrowser.Proctoring private WindowVisibility windowVisibility; public IconResource IconResource { get; set; } + public bool IsHandRaised { get; private set; } public string Tooltip { get; set; } + public event ProctoringEventHandler HandLowered; + public event ProctoringEventHandler HandRaised; public event NotificationChangedEventHandler NotificationChanged; public ProctoringController( @@ -86,6 +90,7 @@ namespace SafeExamBrowser.Proctoring this.settings = settings; this.windowVisibility = settings.WindowVisibility; + server.HandConfirmed += Server_HandConfirmed; server.ProctoringConfigurationReceived += Server_ProctoringConfigurationReceived; server.ProctoringInstructionReceived += Server_ProctoringInstructionReceived; @@ -110,11 +115,51 @@ namespace SafeExamBrowser.Proctoring } } + public void LowerHand() + { + var response = server.LowerHand(); + + if (response.Success) + { + IsHandRaised = false; + HandLowered?.Invoke(); + logger.Info("Hand lowered."); + } + else + { + logger.Error($"Failed to send lower hand notification to server! Message: {response.Message}."); + } + } + + public void RaiseHand(string message = null) + { + var response = server.RaiseHand(message); + + if (response.Success) + { + IsHandRaised = true; + HandRaised?.Invoke(); + logger.Info("Hand raised."); + } + else + { + logger.Error($"Failed to send raise hand notification to server! Message: {response.Message}."); + } + } + public void Terminate() { StopProctoring(); } + private void Server_HandConfirmed() + { + logger.Info("Hand confirmation received."); + + IsHandRaised = false; + HandLowered?.Invoke(); + } + private void Server_ProctoringInstructionReceived(ProctoringInstructionEventArgs args) { logger.Info("Proctoring instruction received."); diff --git a/SafeExamBrowser.Server.Contracts/Events/ServerEventHandler.cs b/SafeExamBrowser.Server.Contracts/Events/ServerEventHandler.cs new file mode 100644 index 00000000..f5a579bf --- /dev/null +++ b/SafeExamBrowser.Server.Contracts/Events/ServerEventHandler.cs @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2021 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.Server.Contracts.Events +{ + /// + /// The default event handler for server events. + /// + public delegate void ServerEventHandler(); +} diff --git a/SafeExamBrowser.Server.Contracts/IServerProxy.cs b/SafeExamBrowser.Server.Contracts/IServerProxy.cs index 5c4d8456..8e3820b1 100644 --- a/SafeExamBrowser.Server.Contracts/IServerProxy.cs +++ b/SafeExamBrowser.Server.Contracts/IServerProxy.cs @@ -20,17 +20,22 @@ namespace SafeExamBrowser.Server.Contracts public interface IServerProxy { /// - /// Event fired when the server receives new proctoring configuration values. + /// Event fired when the proxy receives a confirmation for a raise hand notification. + /// + event ServerEventHandler HandConfirmed; + + /// + /// Event fired when the proxy receives new proctoring configuration values. /// event ProctoringConfigurationReceivedEventHandler ProctoringConfigurationReceived; /// - /// Event fired when the server receives a proctoring instruction. + /// Event fired when the proxy receives a proctoring instruction. /// event ProctoringInstructionReceivedEventHandler ProctoringInstructionReceived; /// - /// Event fired when the server detects an instruction to terminate SEB. + /// Event fired when the proxy detects an instruction to terminate SEB. /// event TerminationRequestedEventHandler TerminationRequested; @@ -69,6 +74,11 @@ namespace SafeExamBrowser.Server.Contracts /// void Initialize(string api, string connectionToken, string examId, string oauth2Token, ServerSettings settings); + /// + /// Sends a lower hand notification to the server. + /// + ServerResponse LowerHand(); + /// /// Sends the given user session identifier of a LMS and thus establishes a connection with the server. /// @@ -83,5 +93,10 @@ namespace SafeExamBrowser.Server.Contracts /// Stops sending ping and log data to the server. /// void StopConnectivity(); + + /// + /// Sends a raise hand notification to the server. + /// + ServerResponse RaiseHand(string message = default(string)); } } diff --git a/SafeExamBrowser.Server.Contracts/SafeExamBrowser.Server.Contracts.csproj b/SafeExamBrowser.Server.Contracts/SafeExamBrowser.Server.Contracts.csproj index de06a5ec..68bca326 100644 --- a/SafeExamBrowser.Server.Contracts/SafeExamBrowser.Server.Contracts.csproj +++ b/SafeExamBrowser.Server.Contracts/SafeExamBrowser.Server.Contracts.csproj @@ -59,6 +59,7 @@ + diff --git a/SafeExamBrowser.Server/Data/Attributes.cs b/SafeExamBrowser.Server/Data/Attributes.cs index af6fa223..19b963ca 100644 --- a/SafeExamBrowser.Server/Data/Attributes.cs +++ b/SafeExamBrowser.Server/Data/Attributes.cs @@ -13,9 +13,11 @@ namespace SafeExamBrowser.Server.Data internal class Attributes { internal bool AllowChat { get; set; } + internal int Id { get; set; } internal ProctoringInstructionEventArgs Instruction { get; set; } internal bool ReceiveAudio { get; set; } internal bool ReceiveVideo { get; set; } + internal string Type { get; set; } internal Attributes() { diff --git a/SafeExamBrowser.Server/Data/Instructions.cs b/SafeExamBrowser.Server/Data/Instructions.cs index e135c5dc..914ff09e 100644 --- a/SafeExamBrowser.Server/Data/Instructions.cs +++ b/SafeExamBrowser.Server/Data/Instructions.cs @@ -10,6 +10,7 @@ namespace SafeExamBrowser.Server.Data { internal sealed class Instructions { + internal const string NOTIFICATION_CONFIRM = "NOTIFICATION_CONFIRM"; internal const string PROCTORING = "SEB_PROCTORING"; internal const string PROCTORING_RECONFIGURATION = "SEB_RECONFIGURE_SETTINGS"; internal const string QUIT = "SEB_QUIT"; diff --git a/SafeExamBrowser.Server/Parser.cs b/SafeExamBrowser.Server/Parser.cs index 263392cc..430d0184 100644 --- a/SafeExamBrowser.Server/Parser.cs +++ b/SafeExamBrowser.Server/Parser.cs @@ -179,6 +179,9 @@ namespace SafeExamBrowser.Server switch (instruction) { + case Instructions.NOTIFICATION_CONFIRM: + ParseNotificationConfirmation(attributes, attributesJson); + break; case Instructions.PROCTORING: ParseProctoringInstruction(attributes, attributesJson); break; @@ -190,6 +193,19 @@ namespace SafeExamBrowser.Server return attributes; } + private void ParseNotificationConfirmation(Attributes attributes, JObject attributesJson) + { + if (attributesJson.ContainsKey("id")) + { + attributes.Id = attributesJson["id"].Value(); + } + + if (attributesJson.ContainsKey("type")) + { + attributes.Type = attributesJson["type"].Value(); + } + } + private void ParseProctoringInstruction(Attributes attributes, JObject attributesJson) { var provider = attributesJson["service-type"].Value(); diff --git a/SafeExamBrowser.Server/ServerProxy.cs b/SafeExamBrowser.Server/ServerProxy.cs index cad618ed..d56afe18 100644 --- a/SafeExamBrowser.Server/ServerProxy.cs +++ b/SafeExamBrowser.Server/ServerProxy.cs @@ -40,6 +40,7 @@ namespace SafeExamBrowser.Server private int currentPowerSupplyValue; private int currentWlanValue; private string examId; + private int handNotificationId; private HttpClient httpClient; private ConcurrentQueue instructionConfirmations; private ILogger logger; @@ -53,6 +54,7 @@ namespace SafeExamBrowser.Server private ServerSettings settings; private IWirelessAdapter wirelessAdapter; + public event ServerEventHandler HandConfirmed; public event ProctoringConfigurationReceivedEventHandler ProctoringConfigurationReceived; public event ProctoringInstructionReceivedEventHandler ProctoringInstructionReceived; public event TerminationRequestedEventHandler TerminationRequested; @@ -234,11 +236,64 @@ namespace SafeExamBrowser.Server Initialize(settings); } + public ServerResponse LowerHand() + { + var authorization = ("Authorization", $"Bearer {oauth2Token}"); + var contentType = "application/json;charset=UTF-8"; + var token = ("SEBConnectionToken", connectionToken); + var json = new JObject + { + ["type"] = "NOTIFICATION_CONFIRMED", + ["timestamp"] = DateTime.Now.ToUnixTimestamp(), + ["numericValue"] = handNotificationId, + }; + var content = json.ToString(); + var success = TryExecute(HttpMethod.Post, api.LogEndpoint, out var response, content, contentType, authorization, token); + + if (success) + { + logger.Info("Successfully sent lower hand notification."); + } + else + { + logger.Error("Failed to send lower hand notification!"); + } + + return new ServerResponse(success, response.ToLogString()); + } + public void Notify(ILogContent content) { logContent.Enqueue(content); } + public ServerResponse RaiseHand(string message = null) + { + var authorization = ("Authorization", $"Bearer {oauth2Token}"); + var contentType = "application/json;charset=UTF-8"; + var token = ("SEBConnectionToken", connectionToken); + var json = new JObject + { + ["type"] = "NOTIFICATION", + ["timestamp"] = DateTime.Now.ToUnixTimestamp(), + ["numericValue"] = ++handNotificationId, + ["text"] = $" {message}" + }; + var content = json.ToString(); + var success = TryExecute(HttpMethod.Post, api.LogEndpoint, out var response, content, contentType, authorization, token); + + if (success) + { + logger.Info("Successfully sent raise hand notification."); + } + else + { + logger.Error("Failed to send raise hand notification!"); + } + + return new ServerResponse(success, response.ToLogString()); + } + public ServerResponse SendSessionIdentifier(string identifier) { var authorization = ("Authorization", $"Bearer {oauth2Token}"); @@ -362,6 +417,9 @@ namespace SafeExamBrowser.Server { switch (instruction) { + case Instructions.NOTIFICATION_CONFIRM when attributes.Type == "raisehand" && attributes.Id == handNotificationId: + Task.Run(() => HandConfirmed?.Invoke()); + break; case Instructions.PROCTORING: Task.Run(() => ProctoringInstructionReceived?.Invoke(attributes.Instruction)); break; diff --git a/SafeExamBrowser.Settings/Proctoring/ProctoringSettings.cs b/SafeExamBrowser.Settings/Proctoring/ProctoringSettings.cs index 00ebb799..7adc0fbe 100644 --- a/SafeExamBrowser.Settings/Proctoring/ProctoringSettings.cs +++ b/SafeExamBrowser.Settings/Proctoring/ProctoringSettings.cs @@ -21,11 +21,21 @@ namespace SafeExamBrowser.Settings.Proctoring /// public bool Enabled { get; set; } + /// + /// Determines whether the message input for the raise hand notification will be forced. + /// + public bool ForceRaiseHandMessage { get; set; } + /// /// All settings for remote proctoring with Jitsi Meet. /// public JitsiMeetSettings JitsiMeet { get; set; } + /// + /// Determines whether the raise hand notification will be shown in the shell. + /// + public bool ShowRaiseHandNotification { get; set; } + /// /// Determines whether the proctoring notification will be shown in the taskbar. /// diff --git a/SafeExamBrowser.UserInterface.Contracts/IUserInterfaceFactory.cs b/SafeExamBrowser.UserInterface.Contracts/IUserInterfaceFactory.cs index d55d4664..418ad793 100644 --- a/SafeExamBrowser.UserInterface.Contracts/IUserInterfaceFactory.cs +++ b/SafeExamBrowser.UserInterface.Contracts/IUserInterfaceFactory.cs @@ -12,8 +12,10 @@ using SafeExamBrowser.Configuration.Contracts; using SafeExamBrowser.Core.Contracts.Notifications; using SafeExamBrowser.I18n.Contracts; using SafeExamBrowser.Logging.Contracts; +using SafeExamBrowser.Proctoring.Contracts; using SafeExamBrowser.Server.Contracts.Data; using SafeExamBrowser.Settings.Browser; +using SafeExamBrowser.Settings.Proctoring; using SafeExamBrowser.SystemComponents.Contracts.Audio; using SafeExamBrowser.SystemComponents.Contracts.Keyboard; using SafeExamBrowser.SystemComponents.Contracts.PowerSupply; @@ -101,6 +103,11 @@ namespace SafeExamBrowser.UserInterface.Contracts /// IProctoringWindow CreateProctoringWindow(IProctoringControl control); + /// + /// Creates a new notification control for the raise hand functionality of a remote proctoring session. + /// + INotificationControl CreateRaiseHandControl(IProctoringController controller, Location location, ProctoringSettings settings); + /// /// Creates a new runtime window which runs on its own thread. /// diff --git a/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj b/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj index eaa90849..85db6dc3 100644 --- a/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj +++ b/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj @@ -125,6 +125,10 @@ {64ea30fb-11d4-436a-9c2b-88566285363e} SafeExamBrowser.Logging.Contracts + + {8e52bd1c-0540-4f16-b181-6665d43f7a7b} + SafeExamBrowser.Proctoring.Contracts + {db701e6f-bddc-4cec-b662-335a9dc11809} SafeExamBrowser.Server.Contracts diff --git a/SafeExamBrowser.UserInterface.Desktop/Controls/ActionCenter/RaiseHandControl.xaml b/SafeExamBrowser.UserInterface.Desktop/Controls/ActionCenter/RaiseHandControl.xaml new file mode 100644 index 00000000..b342314f --- /dev/null +++ b/SafeExamBrowser.UserInterface.Desktop/Controls/ActionCenter/RaiseHandControl.xaml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SafeExamBrowser.UserInterface.Desktop/Controls/ActionCenter/RaiseHandControl.xaml.cs b/SafeExamBrowser.UserInterface.Desktop/Controls/ActionCenter/RaiseHandControl.xaml.cs new file mode 100644 index 00000000..2588fa54 --- /dev/null +++ b/SafeExamBrowser.UserInterface.Desktop/Controls/ActionCenter/RaiseHandControl.xaml.cs @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2021 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.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Input; +using System.Windows.Media; +using SafeExamBrowser.I18n.Contracts; +using SafeExamBrowser.Proctoring.Contracts; +using SafeExamBrowser.Settings.Proctoring; +using SafeExamBrowser.UserInterface.Contracts.Shell; + +namespace SafeExamBrowser.UserInterface.Desktop.Controls.ActionCenter +{ + public partial class RaiseHandControl : UserControl, INotificationControl + { + private readonly IProctoringController controller; + private readonly ProctoringSettings settings; + private readonly IText text; + + public RaiseHandControl(IProctoringController controller, ProctoringSettings settings, IText text) + { + this.controller = controller; + this.settings = settings; + this.text = text; + + InitializeComponent(); + InitializeRaiseHandControl(); + } + + private void InitializeRaiseHandControl() + { + var originalBrush = Grid.Background; + + controller.HandLowered += () => Dispatcher.Invoke(ShowLowered); + controller.HandRaised += () => Dispatcher.Invoke(ShowRaised); + + HandButton.Click += RaiseHandButton_Click; + HandButtonText.Text = text.Get(TextKey.Notification_ProctoringRaiseHand); + + NotificationButton.MouseLeave += (o, args) => Task.Delay(250).ContinueWith(_ => Dispatcher.Invoke(() => Popup.IsOpen = Popup.IsMouseOver)); + NotificationButton.PreviewMouseLeftButtonUp += NotificationButton_PreviewMouseLeftButtonUp; + NotificationButton.PreviewMouseRightButtonUp += NotificationButton_PreviewMouseRightButtonUp; + NotificationButton.ToolTip = text.Get(TextKey.Notification_ProctoringHandLowered); + + Popup.CustomPopupPlacementCallback = new CustomPopupPlacementCallback(Popup_PlacementCallback); + Popup.MouseLeave += (o, args) => Task.Delay(250).ContinueWith(_ => Dispatcher.Invoke(() => Popup.IsOpen = IsMouseOver)); + Popup.Opened += (o, args) => Grid.Background = Brushes.Gray; + Popup.Closed += (o, args) => Grid.Background = originalBrush; + } + + private void NotificationButton_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e) + { + if (settings.ForceRaiseHandMessage || Popup.IsOpen) + { + Popup.IsOpen = !Popup.IsOpen; + } + else + { + ToggleHand(); + } + } + + private void NotificationButton_PreviewMouseRightButtonUp(object sender, MouseButtonEventArgs e) + { + Popup.IsOpen = !Popup.IsOpen; + } + + private CustomPopupPlacement[] Popup_PlacementCallback(Size popupSize, Size targetSize, Point offset) + { + return new[] + { + new CustomPopupPlacement(new Point(targetSize.Width / 2 - popupSize.Width / 2, -popupSize.Height), PopupPrimaryAxis.None) + }; + } + + private void RaiseHandButton_Click(object sender, RoutedEventArgs e) + { + ToggleHand(); + } + + private void ToggleHand() + { + if (controller.IsHandRaised) + { + controller.LowerHand(); + } + else + { + controller.RaiseHand(Message.Text); + Message.Clear(); + } + } + + private void ShowLowered() + { + HandButtonText.Text = text.Get(TextKey.Notification_ProctoringRaiseHand); + NotificationButton.ToolTip = text.Get(TextKey.Notification_ProctoringHandLowered); + TextBlock.Text = "L"; + } + + private void ShowRaised() + { + HandButtonText.Text = text.Get(TextKey.Notification_ProctoringLowerHand); + NotificationButton.ToolTip = text.Get(TextKey.Notification_ProctoringHandRaised); + TextBlock.Text = "R"; + } + } +} diff --git a/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/RaiseHandControl.xaml b/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/RaiseHandControl.xaml new file mode 100644 index 00000000..a6b55191 --- /dev/null +++ b/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/RaiseHandControl.xaml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/RaiseHandControl.xaml.cs b/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/RaiseHandControl.xaml.cs new file mode 100644 index 00000000..f60cebc5 --- /dev/null +++ b/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/RaiseHandControl.xaml.cs @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2021 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.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Input; +using System.Windows.Media; +using SafeExamBrowser.I18n.Contracts; +using SafeExamBrowser.Proctoring.Contracts; +using SafeExamBrowser.Settings.Proctoring; +using SafeExamBrowser.UserInterface.Contracts.Shell; + +namespace SafeExamBrowser.UserInterface.Desktop.Controls.Taskbar +{ + public partial class RaiseHandControl : UserControl, INotificationControl + { + private readonly IProctoringController controller; + private readonly ProctoringSettings settings; + private readonly IText text; + + public RaiseHandControl(IProctoringController controller, ProctoringSettings settings, IText text) + { + this.controller = controller; + this.settings = settings; + this.text = text; + + InitializeComponent(); + InitializeRaiseHandControl(); + } + + private void InitializeRaiseHandControl() + { + var originalBrush = NotificationButton.Background; + + controller.HandLowered += () => Dispatcher.Invoke(ShowLowered); + controller.HandRaised += () => Dispatcher.Invoke(ShowRaised); + + HandButton.Click += RaiseHandButton_Click; + HandButtonText.Text = text.Get(TextKey.Notification_ProctoringRaiseHand); + + NotificationButton.MouseLeave += (o, args) => Task.Delay(250).ContinueWith(_ => Dispatcher.Invoke(() => Popup.IsOpen = Popup.IsMouseOver)); + NotificationButton.PreviewMouseLeftButtonUp += NotificationButton_PreviewMouseLeftButtonUp; + NotificationButton.PreviewMouseRightButtonUp += NotificationButton_PreviewMouseRightButtonUp; + NotificationButton.ToolTip = text.Get(TextKey.Notification_ProctoringHandLowered); + + Popup.CustomPopupPlacementCallback = new CustomPopupPlacementCallback(Popup_PlacementCallback); + Popup.MouseLeave += (o, args) => Task.Delay(250).ContinueWith(_ => Dispatcher.Invoke(() => Popup.IsOpen = IsMouseOver)); + Popup.Opened += (o, args) => + { + Background = Brushes.LightGray; + NotificationButton.Background = Brushes.LightGray; + }; + + Popup.Closed += (o, args) => + { + Background = originalBrush; + NotificationButton.Background = originalBrush; + }; + } + + private void NotificationButton_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e) + { + if (settings.ForceRaiseHandMessage || Popup.IsOpen) + { + Popup.IsOpen = !Popup.IsOpen; + } + else + { + ToggleHand(); + } + } + + private void NotificationButton_PreviewMouseRightButtonUp(object sender, MouseButtonEventArgs e) + { + Popup.IsOpen = !Popup.IsOpen; + } + + private CustomPopupPlacement[] Popup_PlacementCallback(Size popupSize, Size targetSize, Point offset) + { + return new[] + { + new CustomPopupPlacement(new Point(targetSize.Width / 2 - popupSize.Width / 2, -popupSize.Height), PopupPrimaryAxis.None) + }; + } + + private void RaiseHandButton_Click(object sender, RoutedEventArgs e) + { + ToggleHand(); + } + + private void ToggleHand() + { + if (controller.IsHandRaised) + { + controller.LowerHand(); + } + else + { + controller.RaiseHand(Message.Text); + Message.Clear(); + } + } + + private void ShowLowered() + { + HandButtonText.Text = text.Get(TextKey.Notification_ProctoringRaiseHand); + NotificationButton.ToolTip = text.Get(TextKey.Notification_ProctoringHandLowered); + TextBlock.Text = "L"; + } + + private void ShowRaised() + { + HandButtonText.Text = text.Get(TextKey.Notification_ProctoringLowerHand); + NotificationButton.ToolTip = text.Get(TextKey.Notification_ProctoringHandRaised); + TextBlock.Text = "R"; + } + } +} diff --git a/SafeExamBrowser.UserInterface.Desktop/SafeExamBrowser.UserInterface.Desktop.csproj b/SafeExamBrowser.UserInterface.Desktop/SafeExamBrowser.UserInterface.Desktop.csproj index 1e66c46b..7e9ae454 100644 --- a/SafeExamBrowser.UserInterface.Desktop/SafeExamBrowser.UserInterface.Desktop.csproj +++ b/SafeExamBrowser.UserInterface.Desktop/SafeExamBrowser.UserInterface.Desktop.csproj @@ -67,6 +67,12 @@ + + RaiseHandControl.xaml + + + RaiseHandControl.xaml + AboutWindow.xaml @@ -193,6 +199,14 @@ MSBuild:Compile Designer + + MSBuild:Compile + Designer + + + Designer + MSBuild:Compile + Designer MSBuild:Compile @@ -503,6 +517,10 @@ {64ea30fb-11d4-436a-9c2b-88566285363e} SafeExamBrowser.Logging.Contracts + + {8e52bd1c-0540-4f16-b181-6665d43f7a7b} + SafeExamBrowser.Proctoring.Contracts + {DB701E6F-BDDC-4CEC-B662-335A9DC11809} SafeExamBrowser.Server.Contracts diff --git a/SafeExamBrowser.UserInterface.Desktop/UserInterfaceFactory.cs b/SafeExamBrowser.UserInterface.Desktop/UserInterfaceFactory.cs index bbef2ebf..8eefe160 100644 --- a/SafeExamBrowser.UserInterface.Desktop/UserInterfaceFactory.cs +++ b/SafeExamBrowser.UserInterface.Desktop/UserInterfaceFactory.cs @@ -16,8 +16,10 @@ using SafeExamBrowser.Configuration.Contracts; using SafeExamBrowser.Core.Contracts.Notifications; using SafeExamBrowser.I18n.Contracts; using SafeExamBrowser.Logging.Contracts; +using SafeExamBrowser.Proctoring.Contracts; using SafeExamBrowser.Server.Contracts.Data; using SafeExamBrowser.Settings.Browser; +using SafeExamBrowser.Settings.Proctoring; using SafeExamBrowser.SystemComponents.Contracts.Audio; using SafeExamBrowser.SystemComponents.Contracts.Keyboard; using SafeExamBrowser.SystemComponents.Contracts.PowerSupply; @@ -168,6 +170,18 @@ namespace SafeExamBrowser.UserInterface.Desktop return Application.Current.Dispatcher.Invoke(() => new ProctoringWindow(control)); } + public INotificationControl CreateRaiseHandControl(IProctoringController controller, Location location, ProctoringSettings settings) + { + if (location == Location.ActionCenter) + { + return new Controls.ActionCenter.RaiseHandControl(controller, settings, text); + } + else + { + return new Controls.Taskbar.RaiseHandControl(controller, settings, text); + } + } + public IRuntimeWindow CreateRuntimeWindow(AppConfig appConfig) { return Application.Current.Dispatcher.Invoke(() => new RuntimeWindow(appConfig, text)); diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenter/RaiseHandControl.xaml b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenter/RaiseHandControl.xaml new file mode 100644 index 00000000..242ee73a --- /dev/null +++ b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenter/RaiseHandControl.xaml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenter/RaiseHandControl.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenter/RaiseHandControl.xaml.cs new file mode 100644 index 00000000..b1e4dba4 --- /dev/null +++ b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenter/RaiseHandControl.xaml.cs @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2021 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.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Input; +using System.Windows.Media; +using SafeExamBrowser.I18n.Contracts; +using SafeExamBrowser.Proctoring.Contracts; +using SafeExamBrowser.Settings.Proctoring; +using SafeExamBrowser.UserInterface.Contracts.Shell; + +namespace SafeExamBrowser.UserInterface.Mobile.Controls.ActionCenter +{ + public partial class RaiseHandControl : UserControl, INotificationControl + { + private readonly IProctoringController controller; + private readonly ProctoringSettings settings; + private readonly IText text; + + public RaiseHandControl(IProctoringController controller, ProctoringSettings settings, IText text) + { + this.controller = controller; + this.settings = settings; + this.text = text; + + InitializeComponent(); + InitializeRaiseHandControl(); + } + + private void InitializeRaiseHandControl() + { + var originalBrush = Grid.Background; + + controller.HandLowered += () => Dispatcher.Invoke(ShowLowered); + controller.HandRaised += () => Dispatcher.Invoke(ShowRaised); + + HandButton.Click += RaiseHandButton_Click; + HandButtonText.Text = text.Get(TextKey.Notification_ProctoringRaiseHand); + + NotificationButton.MouseLeave += (o, args) => Task.Delay(250).ContinueWith(_ => Dispatcher.Invoke(() => Popup.IsOpen = Popup.IsMouseOver)); + NotificationButton.PreviewMouseLeftButtonUp += NotificationButton_PreviewMouseLeftButtonUp; + NotificationButton.PreviewMouseRightButtonUp += NotificationButton_PreviewMouseRightButtonUp; + NotificationButton.ToolTip = text.Get(TextKey.Notification_ProctoringHandLowered); + + Popup.CustomPopupPlacementCallback = new CustomPopupPlacementCallback(Popup_PlacementCallback); + Popup.MouseLeave += (o, args) => Task.Delay(250).ContinueWith(_ => Dispatcher.Invoke(() => Popup.IsOpen = IsMouseOver)); + Popup.Opened += (o, args) => Grid.Background = Brushes.Gray; + Popup.Closed += (o, args) => Grid.Background = originalBrush; + } + + private void NotificationButton_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e) + { + if (settings.ForceRaiseHandMessage || Popup.IsOpen) + { + Popup.IsOpen = !Popup.IsOpen; + } + else + { + ToggleHand(); + } + } + + private void NotificationButton_PreviewMouseRightButtonUp(object sender, MouseButtonEventArgs e) + { + Popup.IsOpen = !Popup.IsOpen; + } + + private CustomPopupPlacement[] Popup_PlacementCallback(Size popupSize, Size targetSize, Point offset) + { + return new[] + { + new CustomPopupPlacement(new Point(targetSize.Width / 2 - popupSize.Width / 2, -popupSize.Height), PopupPrimaryAxis.None) + }; + } + + private void RaiseHandButton_Click(object sender, RoutedEventArgs e) + { + ToggleHand(); + } + + private void ToggleHand() + { + if (controller.IsHandRaised) + { + controller.LowerHand(); + } + else + { + controller.RaiseHand(Message.Text); + Message.Clear(); + } + } + + private void ShowLowered() + { + HandButtonText.Text = text.Get(TextKey.Notification_ProctoringRaiseHand); + NotificationButton.ToolTip = text.Get(TextKey.Notification_ProctoringHandLowered); + TextBlock.Text = "L"; + } + + private void ShowRaised() + { + HandButtonText.Text = text.Get(TextKey.Notification_ProctoringLowerHand); + NotificationButton.ToolTip = text.Get(TextKey.Notification_ProctoringHandRaised); + TextBlock.Text = "R"; + } + } +} diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/Taskbar/RaiseHandControl.xaml b/SafeExamBrowser.UserInterface.Mobile/Controls/Taskbar/RaiseHandControl.xaml new file mode 100644 index 00000000..b7bbd45f --- /dev/null +++ b/SafeExamBrowser.UserInterface.Mobile/Controls/Taskbar/RaiseHandControl.xaml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/Taskbar/RaiseHandControl.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/Controls/Taskbar/RaiseHandControl.xaml.cs new file mode 100644 index 00000000..67554b61 --- /dev/null +++ b/SafeExamBrowser.UserInterface.Mobile/Controls/Taskbar/RaiseHandControl.xaml.cs @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2021 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.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Input; +using System.Windows.Media; +using SafeExamBrowser.I18n.Contracts; +using SafeExamBrowser.Proctoring.Contracts; +using SafeExamBrowser.Settings.Proctoring; +using SafeExamBrowser.UserInterface.Contracts.Shell; + +namespace SafeExamBrowser.UserInterface.Mobile.Controls.Taskbar +{ + public partial class RaiseHandControl : UserControl, INotificationControl + { + private readonly IProctoringController controller; + private readonly ProctoringSettings settings; + private readonly IText text; + + public RaiseHandControl(IProctoringController controller, ProctoringSettings settings, IText text) + { + this.controller = controller; + this.settings = settings; + this.text = text; + + InitializeComponent(); + InitializeRaiseHandControl(); + } + + private void InitializeRaiseHandControl() + { + var originalBrush = NotificationButton.Background; + + controller.HandLowered += () => Dispatcher.Invoke(ShowLowered); + controller.HandRaised += () => Dispatcher.Invoke(ShowRaised); + + HandButton.Click += RaiseHandButton_Click; + HandButtonText.Text = text.Get(TextKey.Notification_ProctoringRaiseHand); + + NotificationButton.MouseLeave += (o, args) => Task.Delay(250).ContinueWith(_ => Dispatcher.Invoke(() => Popup.IsOpen = Popup.IsMouseOver)); + NotificationButton.PreviewMouseLeftButtonUp += NotificationButton_PreviewMouseLeftButtonUp; + NotificationButton.PreviewMouseRightButtonUp += NotificationButton_PreviewMouseRightButtonUp; + NotificationButton.ToolTip = text.Get(TextKey.Notification_ProctoringHandLowered); + + Popup.CustomPopupPlacementCallback = new CustomPopupPlacementCallback(Popup_PlacementCallback); + Popup.MouseLeave += (o, args) => Task.Delay(250).ContinueWith(_ => Dispatcher.Invoke(() => Popup.IsOpen = IsMouseOver)); + Popup.Opened += (o, args) => + { + Background = Brushes.LightGray; + NotificationButton.Background = Brushes.LightGray; + }; + + Popup.Closed += (o, args) => + { + Background = originalBrush; + NotificationButton.Background = originalBrush; + }; + } + + private void NotificationButton_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e) + { + if (settings.ForceRaiseHandMessage || Popup.IsOpen) + { + Popup.IsOpen = !Popup.IsOpen; + } + else + { + ToggleHand(); + } + } + + private void NotificationButton_PreviewMouseRightButtonUp(object sender, MouseButtonEventArgs e) + { + Popup.IsOpen = !Popup.IsOpen; + } + + private CustomPopupPlacement[] Popup_PlacementCallback(Size popupSize, Size targetSize, Point offset) + { + return new[] + { + new CustomPopupPlacement(new Point(targetSize.Width / 2 - popupSize.Width / 2, -popupSize.Height), PopupPrimaryAxis.None) + }; + } + + private void RaiseHandButton_Click(object sender, RoutedEventArgs e) + { + ToggleHand(); + } + + private void ToggleHand() + { + if (controller.IsHandRaised) + { + controller.LowerHand(); + } + else + { + controller.RaiseHand(Message.Text); + Message.Clear(); + } + } + + private void ShowLowered() + { + HandButtonText.Text = text.Get(TextKey.Notification_ProctoringRaiseHand); + NotificationButton.ToolTip = text.Get(TextKey.Notification_ProctoringHandLowered); + TextBlock.Text = "L"; + } + + private void ShowRaised() + { + HandButtonText.Text = text.Get(TextKey.Notification_ProctoringLowerHand); + NotificationButton.ToolTip = text.Get(TextKey.Notification_ProctoringHandRaised); + TextBlock.Text = "R"; + } + } +} diff --git a/SafeExamBrowser.UserInterface.Mobile/SafeExamBrowser.UserInterface.Mobile.csproj b/SafeExamBrowser.UserInterface.Mobile/SafeExamBrowser.UserInterface.Mobile.csproj index 0bef8b77..3c0091b6 100644 --- a/SafeExamBrowser.UserInterface.Mobile/SafeExamBrowser.UserInterface.Mobile.csproj +++ b/SafeExamBrowser.UserInterface.Mobile/SafeExamBrowser.UserInterface.Mobile.csproj @@ -68,6 +68,12 @@ + + RaiseHandControl.xaml + + + RaiseHandControl.xaml + AboutWindow.xaml @@ -217,6 +223,10 @@ {64ea30fb-11d4-436a-9c2b-88566285363e} SafeExamBrowser.Logging.Contracts + + {8E52BD1C-0540-4F16-B181-6665D43F7A7B} + SafeExamBrowser.Proctoring.Contracts + {DB701E6F-BDDC-4CEC-B662-335A9DC11809} SafeExamBrowser.Server.Contracts @@ -247,6 +257,14 @@ MSBuild:Compile Designer + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + MSBuild:Compile Designer diff --git a/SafeExamBrowser.UserInterface.Mobile/UserInterfaceFactory.cs b/SafeExamBrowser.UserInterface.Mobile/UserInterfaceFactory.cs index 72425f5c..8efb520d 100644 --- a/SafeExamBrowser.UserInterface.Mobile/UserInterfaceFactory.cs +++ b/SafeExamBrowser.UserInterface.Mobile/UserInterfaceFactory.cs @@ -16,8 +16,10 @@ using SafeExamBrowser.Configuration.Contracts; using SafeExamBrowser.Core.Contracts.Notifications; using SafeExamBrowser.I18n.Contracts; using SafeExamBrowser.Logging.Contracts; +using SafeExamBrowser.Proctoring.Contracts; using SafeExamBrowser.Server.Contracts.Data; using SafeExamBrowser.Settings.Browser; +using SafeExamBrowser.Settings.Proctoring; using SafeExamBrowser.SystemComponents.Contracts.Audio; using SafeExamBrowser.SystemComponents.Contracts.Keyboard; using SafeExamBrowser.SystemComponents.Contracts.PowerSupply; @@ -168,6 +170,18 @@ namespace SafeExamBrowser.UserInterface.Mobile return Application.Current.Dispatcher.Invoke(() => new ProctoringWindow(control)); } + public INotificationControl CreateRaiseHandControl(IProctoringController controller, Location location, ProctoringSettings settings) + { + if (location == Location.ActionCenter) + { + return new Controls.ActionCenter.RaiseHandControl(controller, settings, text); + } + else + { + return new Controls.Taskbar.RaiseHandControl(controller, settings, text); + } + } + public IRuntimeWindow CreateRuntimeWindow(AppConfig appConfig) { return Application.Current.Dispatcher.Invoke(() => new RuntimeWindow(appConfig, text));