diff --git a/SafeExamBrowser.Client.UnitTests/Operations/ProctoringOperationTests.cs b/SafeExamBrowser.Client.UnitTests/Operations/ProctoringOperationTests.cs index c900f59b..9e5221cf 100644 --- a/SafeExamBrowser.Client.UnitTests/Operations/ProctoringOperationTests.cs +++ b/SafeExamBrowser.Client.UnitTests/Operations/ProctoringOperationTests.cs @@ -27,7 +27,8 @@ namespace SafeExamBrowser.Client.UnitTests.Operations private ClientContext context; private Mock controller; private Mock logger; - private Mock notification; + private Mock notification1; + private Mock notification2; private AppSettings settings; private Mock taskbar; private Mock uiFactory; @@ -41,13 +42,15 @@ namespace SafeExamBrowser.Client.UnitTests.Operations context = new ClientContext(); controller = new Mock(); logger = new Mock(); - notification = new Mock(); + notification1 = new Mock(); + notification2 = new Mock(); settings = new AppSettings(); taskbar = new Mock(); uiFactory = new Mock(); context.Settings = settings; - sut = new ProctoringOperation(actionCenter.Object, context, controller.Object, logger.Object, notification.Object, taskbar.Object, uiFactory.Object); + controller.SetupGet(c => c.Notifications).Returns(new[] { notification1.Object, notification2.Object }); + sut = new ProctoringOperation(actionCenter.Object, context, controller.Object, logger.Object, taskbar.Object, uiFactory.Object); } [TestMethod] @@ -58,12 +61,13 @@ namespace SafeExamBrowser.Client.UnitTests.Operations Assert.AreEqual(OperationResult.Success, sut.Perform()); - actionCenter.Verify(a => a.AddNotificationControl(It.IsAny()), Times.Once); + actionCenter.Verify(a => a.AddNotificationControl(It.IsAny()), Times.Exactly(2)); controller.Verify(c => c.Initialize(It.Is(s => s == settings.Proctoring))); - notification.VerifyNoOtherCalls(); - taskbar.Verify(t => t.AddNotificationControl(It.IsAny()), Times.Once); - uiFactory.Verify(u => u.CreateNotificationControl(It.Is(n => n == notification.Object), Location.ActionCenter), Times.Once); - uiFactory.Verify(u => u.CreateNotificationControl(It.Is(n => n == notification.Object), Location.Taskbar), Times.Once); + notification1.VerifyNoOtherCalls(); + notification2.VerifyNoOtherCalls(); + taskbar.Verify(t => t.AddNotificationControl(It.IsAny()), Times.Exactly(2)); + uiFactory.Verify(u => u.CreateNotificationControl(It.IsAny(), Location.ActionCenter), Times.Exactly(2)); + uiFactory.Verify(u => u.CreateNotificationControl(It.IsAny(), Location.Taskbar), Times.Exactly(2)); } [TestMethod] @@ -75,7 +79,8 @@ namespace SafeExamBrowser.Client.UnitTests.Operations actionCenter.VerifyNoOtherCalls(); controller.VerifyNoOtherCalls(); - notification.VerifyNoOtherCalls(); + notification1.VerifyNoOtherCalls(); + notification2.VerifyNoOtherCalls(); taskbar.VerifyNoOtherCalls(); uiFactory.VerifyNoOtherCalls(); } @@ -89,7 +94,8 @@ namespace SafeExamBrowser.Client.UnitTests.Operations actionCenter.VerifyNoOtherCalls(); controller.Verify(c => c.Terminate(), Times.Once); - notification.Verify(n => n.Terminate(), Times.Once); + notification1.Verify(n => n.Terminate(), Times.Once); + notification2.Verify(n => n.Terminate(), Times.Once); taskbar.VerifyNoOtherCalls(); uiFactory.VerifyNoOtherCalls(); } @@ -103,7 +109,8 @@ namespace SafeExamBrowser.Client.UnitTests.Operations actionCenter.VerifyNoOtherCalls(); controller.VerifyNoOtherCalls(); - notification.VerifyNoOtherCalls(); + notification1.VerifyNoOtherCalls(); + notification2.VerifyNoOtherCalls(); taskbar.VerifyNoOtherCalls(); uiFactory.VerifyNoOtherCalls(); } diff --git a/SafeExamBrowser.Client/CompositionRoot.cs b/SafeExamBrowser.Client/CompositionRoot.cs index bc497bd9..a0fdac3a 100644 --- a/SafeExamBrowser.Client/CompositionRoot.cs +++ b/SafeExamBrowser.Client/CompositionRoot.cs @@ -289,7 +289,7 @@ namespace SafeExamBrowser.Client private IOperation BuildProctoringOperation() { var controller = new ProctoringController(context.AppConfig, new FileSystem(), ModuleLogger(nameof(ProctoringController)), context.Server, text, uiFactory); - var operation = new ProctoringOperation(actionCenter, context, controller, logger, controller, taskbar, uiFactory); + var operation = new ProctoringOperation(actionCenter, context, controller, logger, taskbar, uiFactory); return operation; } diff --git a/SafeExamBrowser.Client/Operations/ProctoringOperation.cs b/SafeExamBrowser.Client/Operations/ProctoringOperation.cs index 293d6068..e1fa9acd 100644 --- a/SafeExamBrowser.Client/Operations/ProctoringOperation.cs +++ b/SafeExamBrowser.Client/Operations/ProctoringOperation.cs @@ -6,7 +6,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -using SafeExamBrowser.Core.Contracts.Notifications; using SafeExamBrowser.Core.Contracts.OperationModel; using SafeExamBrowser.Core.Contracts.OperationModel.Events; using SafeExamBrowser.I18n.Contracts; @@ -23,7 +22,6 @@ namespace SafeExamBrowser.Client.Operations private readonly IActionCenter actionCenter; private readonly IProctoringController controller; private readonly ILogger logger; - private readonly INotification notification; private readonly ITaskbar taskbar; private readonly IUserInterfaceFactory uiFactory; @@ -35,14 +33,12 @@ namespace SafeExamBrowser.Client.Operations ClientContext context, IProctoringController controller, ILogger logger, - INotification notification, ITaskbar taskbar, IUserInterfaceFactory uiFactory) : base(context) { this.actionCenter = actionCenter; this.controller = controller; this.logger = logger; - this.notification = notification; this.taskbar = taskbar; this.uiFactory = uiFactory; } @@ -55,7 +51,6 @@ namespace SafeExamBrowser.Client.Operations StatusChanged?.Invoke(TextKey.OperationStatus_InitializeProctoring); controller.Initialize(Context.Settings.Proctoring); - actionCenter.AddNotificationControl(uiFactory.CreateNotificationControl(notification, Location.ActionCenter)); if (Context.Settings.SessionMode == SessionMode.Server && Context.Settings.Proctoring.ShowRaiseHandNotification) { @@ -63,9 +58,14 @@ namespace SafeExamBrowser.Client.Operations taskbar.AddNotificationControl(uiFactory.CreateRaiseHandControl(controller, Location.Taskbar, Context.Settings.Proctoring)); } - if (Context.Settings.Proctoring.ShowTaskbarNotification) + foreach (var notification in controller.Notifications) { - taskbar.AddNotificationControl(uiFactory.CreateNotificationControl(notification, Location.Taskbar)); + actionCenter.AddNotificationControl(uiFactory.CreateNotificationControl(notification, Location.ActionCenter)); + + if (Context.Settings.Proctoring.ShowTaskbarNotification) + { + taskbar.AddNotificationControl(uiFactory.CreateNotificationControl(notification, Location.Taskbar)); + } } } @@ -80,7 +80,11 @@ namespace SafeExamBrowser.Client.Operations StatusChanged?.Invoke(TextKey.OperationStatus_TerminateProctoring); controller.Terminate(); - notification.Terminate(); + + foreach (var notification in controller.Notifications) + { + notification.Terminate(); + } } return OperationResult.Success; diff --git a/SafeExamBrowser.Proctoring.Contracts/IProctoringController.cs b/SafeExamBrowser.Proctoring.Contracts/IProctoringController.cs index df876001..6fdd4f7c 100644 --- a/SafeExamBrowser.Proctoring.Contracts/IProctoringController.cs +++ b/SafeExamBrowser.Proctoring.Contracts/IProctoringController.cs @@ -6,6 +6,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +using System.Collections.Generic; +using SafeExamBrowser.Core.Contracts.Notifications; using SafeExamBrowser.Proctoring.Contracts.Events; using SafeExamBrowser.Settings.Proctoring; @@ -21,6 +23,11 @@ namespace SafeExamBrowser.Proctoring.Contracts /// bool IsHandRaised { get; } + /// + /// The notifications for all active proctoring providers. + /// + IEnumerable Notifications { get; } + /// /// Fired when the hand has been lowered. /// @@ -44,7 +51,7 @@ namespace SafeExamBrowser.Proctoring.Contracts /// /// Raises the hand, optionally with the given message. /// - void RaiseHand(string message = default(string)); + void RaiseHand(string message = default); /// /// Stops the proctoring functionality. diff --git a/SafeExamBrowser.Proctoring/JitsiMeet/JitsiMeetImplementation.cs b/SafeExamBrowser.Proctoring/JitsiMeet/JitsiMeetImplementation.cs new file mode 100644 index 00000000..7026baba --- /dev/null +++ b/SafeExamBrowser.Proctoring/JitsiMeet/JitsiMeetImplementation.cs @@ -0,0 +1,245 @@ +/* + * Copyright (c) 2023 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.IO; +using System.Reflection; +using System.Threading; +using System.Windows; +using Microsoft.Web.WebView2.Wpf; +using SafeExamBrowser.Configuration.Contracts; +using SafeExamBrowser.Core.Contracts.Notifications.Events; +using SafeExamBrowser.Core.Contracts.Resources.Icons; +using SafeExamBrowser.I18n.Contracts; +using SafeExamBrowser.Logging.Contracts; +using SafeExamBrowser.Server.Contracts.Events; +using SafeExamBrowser.Settings.Proctoring; +using SafeExamBrowser.SystemComponents.Contracts; +using SafeExamBrowser.UserInterface.Contracts; +using SafeExamBrowser.UserInterface.Contracts.Proctoring; + +namespace SafeExamBrowser.Proctoring.JitsiMeet +{ + internal class JitsiMeetImplementation : ProctoringImplementation + { + private readonly AppConfig appConfig; + private readonly IFileSystem fileSystem; + private readonly IModuleLogger logger; + private readonly ProctoringSettings settings; + private readonly IText text; + private readonly IUserInterfaceFactory uiFactory; + + private ProctoringControl control; + private string filePath; + private WindowVisibility initialVisibility; + private IProctoringWindow window; + + internal override string Name => nameof(JitsiMeet); + + public override string Tooltip { get; protected set; } + public override IconResource IconResource { get; protected set; } + + public override event NotificationChangedEventHandler NotificationChanged; + + internal JitsiMeetImplementation( + AppConfig appConfig, + IFileSystem fileSystem, + IModuleLogger logger, + ProctoringSettings settings, + IText text, + IUserInterfaceFactory uiFactory) + { + this.appConfig = appConfig; + this.fileSystem = fileSystem; + this.logger = logger; + this.settings = settings; + this.text = text; + this.uiFactory = uiFactory; + + IconResource = new XamlIconResource { Uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/ProctoringNotification_Inactive.xaml") }; + Tooltip = text.Get(TextKey.Notification_ProctoringInactiveTooltip); + } + + public override void Activate() + { + if (settings.WindowVisibility == WindowVisibility.Visible) + { + window?.BringToForeground(); + } + else if (settings.WindowVisibility == WindowVisibility.AllowToHide || settings.WindowVisibility == WindowVisibility.AllowToShow) + { + window?.Toggle(); + } + } + + public override void Terminate() + { + logger.Info("Terminated proctoring."); + } + + internal override void Initialize() + { + var start = true; + + initialVisibility = settings.WindowVisibility; + settings.JitsiMeet.ServerUrl = Sanitize(settings.JitsiMeet.ServerUrl); + + start &= !string.IsNullOrWhiteSpace(settings.JitsiMeet.RoomName); + start &= !string.IsNullOrWhiteSpace(settings.JitsiMeet.ServerUrl); + + logger.Info("Initialized proctoring."); + + if (start) + { + StartProctoring(); + } + } + + internal override void ProctoringConfigurationReceived(bool allowChat, bool receiveAudio, bool receiveVideo) + { + logger.Info("Proctoring configuration received."); + + settings.JitsiMeet.AllowChat = allowChat; + settings.JitsiMeet.ReceiveAudio = receiveAudio; + settings.JitsiMeet.ReceiveVideo = receiveVideo; + + if (allowChat || receiveVideo) + { + settings.WindowVisibility = WindowVisibility.AllowToHide; + } + else + { + settings.WindowVisibility = initialVisibility; + } + + StopProctoring(); + StartProctoring(); + + logger.Info($"Successfully updated configuration: {nameof(allowChat)}={allowChat}, {nameof(receiveAudio)}={receiveAudio}, {nameof(receiveVideo)}={receiveVideo}."); + } + + internal override void ProctoringInstructionReceived(ProctoringInstructionEventArgs args) + { + logger.Info("Proctoring instruction received."); + + settings.JitsiMeet.RoomName = args.JitsiMeetRoomName; + settings.JitsiMeet.ServerUrl = args.JitsiMeetServerUrl; + settings.JitsiMeet.Token = args.JitsiMeetToken; + + StopProctoring(); + StartProctoring(); + + logger.Info("Successfully processed instruction."); + } + + internal override void StartProctoring() + { + Application.Current.Dispatcher.Invoke(() => + { + try + { + var content = LoadContent(settings); + + filePath = Path.Combine(appConfig.TemporaryDirectory, $"{Path.GetRandomFileName()}_index.html"); + fileSystem.Save(content, filePath); + + control = new ProctoringControl(logger.CloneFor(nameof(ProctoringControl)), settings); + control.CreationProperties = new CoreWebView2CreationProperties { UserDataFolder = appConfig.TemporaryDirectory }; + control.EnsureCoreWebView2Async().ContinueWith(_ => + { + control.Dispatcher.Invoke(() => + { + control.CoreWebView2.Navigate(filePath); + }); + }); + + window = uiFactory.CreateProctoringWindow(control); + window.SetTitle(settings.JitsiMeet.Enabled ? settings.JitsiMeet.Subject : ""); + window.Show(); + + if (settings.WindowVisibility == WindowVisibility.AllowToShow || settings.WindowVisibility == WindowVisibility.Hidden) + { + window.Hide(); + } + + ShowNotificationActive(); + + logger.Info("Started proctoring."); + } + catch (Exception e) + { + logger.Error($"Failed to start proctoring! Reason: {e.Message}", e); + } + }); + } + + internal override void StopProctoring() + { + if (control != default && window != default) + { + control.Dispatcher.Invoke(() => + { + control.ExecuteScriptAsync("api.executeCommand('hangup'); api.dispose();"); + + Thread.Sleep(2000); + + window.Close(); + control = default; + window = default; + fileSystem.Delete(filePath); + + ShowNotificationInactive(); + + logger.Info("Stopped proctoring."); + }); + } + } + + private string LoadContent(ProctoringSettings settings) + { + var assembly = Assembly.GetAssembly(typeof(ProctoringController)); + var path = $"{typeof(ProctoringController).Namespace}.JitsiMeet.index.html"; + + using (var stream = assembly.GetManifestResourceStream(path)) + using (var reader = new StreamReader(stream)) + { + var html = reader.ReadToEnd(); + + html = html.Replace("%%_ALLOW_CHAT_%%", settings.JitsiMeet.AllowChat ? "chat" : ""); + html = html.Replace("%%_ALLOW_CLOSED_CAPTIONS_%%", settings.JitsiMeet.AllowClosedCaptions ? "closedcaptions" : ""); + html = html.Replace("%%_ALLOW_RAISE_HAND_%%", settings.JitsiMeet.AllowRaiseHand ? "raisehand" : ""); + html = html.Replace("%%_ALLOW_RECORDING_%%", settings.JitsiMeet.AllowRecording ? "recording" : ""); + html = html.Replace("%%_ALLOW_TILE_VIEW", settings.JitsiMeet.AllowTileView ? "tileview" : ""); + html = html.Replace("'%_AUDIO_MUTED_%'", settings.JitsiMeet.AudioMuted && settings.WindowVisibility != WindowVisibility.Hidden ? "true" : "false"); + html = html.Replace("'%_AUDIO_ONLY_%'", settings.JitsiMeet.AudioOnly ? "true" : "false"); + html = html.Replace("'%_VIDEO_MUTED_%'", settings.JitsiMeet.VideoMuted && settings.WindowVisibility != WindowVisibility.Hidden ? "true" : "false"); + + return html; + } + } + + private string Sanitize(string serverUrl) + { + return serverUrl?.Replace($"{Uri.UriSchemeHttp}{Uri.SchemeDelimiter}", "").Replace($"{Uri.UriSchemeHttps}{Uri.SchemeDelimiter}", ""); + } + + private void ShowNotificationActive() + { + IconResource = new XamlIconResource { Uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/ProctoringNotification_Active.xaml") }; + Tooltip = text.Get(TextKey.Notification_ProctoringActiveTooltip); + NotificationChanged?.Invoke(); + } + + private void ShowNotificationInactive() + { + IconResource = new XamlIconResource { Uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/ProctoringNotification_Inactive.xaml") }; + Tooltip = text.Get(TextKey.Notification_ProctoringInactiveTooltip); + NotificationChanged?.Invoke(); + } + } +} diff --git a/SafeExamBrowser.Proctoring/ProctoringController.cs b/SafeExamBrowser.Proctoring/ProctoringController.cs index 1e237a4e..977109df 100644 --- a/SafeExamBrowser.Proctoring/ProctoringController.cs +++ b/SafeExamBrowser.Proctoring/ProctoringController.cs @@ -7,15 +7,9 @@ */ using System; -using System.IO; -using System.Reflection; -using System.Threading; -using System.Windows; -using Microsoft.Web.WebView2.Wpf; +using System.Collections.Generic; using SafeExamBrowser.Configuration.Contracts; using SafeExamBrowser.Core.Contracts.Notifications; -using SafeExamBrowser.Core.Contracts.Notifications.Events; -using SafeExamBrowser.Core.Contracts.Resources.Icons; using SafeExamBrowser.I18n.Contracts; using SafeExamBrowser.Logging.Contracts; using SafeExamBrowser.Proctoring.Contracts; @@ -25,32 +19,22 @@ using SafeExamBrowser.Server.Contracts.Events; using SafeExamBrowser.Settings.Proctoring; using SafeExamBrowser.SystemComponents.Contracts; using SafeExamBrowser.UserInterface.Contracts; -using SafeExamBrowser.UserInterface.Contracts.Proctoring; namespace SafeExamBrowser.Proctoring { - public class ProctoringController : IProctoringController, INotification + public class ProctoringController : IProctoringController { - private readonly AppConfig appConfig; - private readonly IFileSystem fileSystem; + private readonly ProctoringFactory factory; private readonly IModuleLogger logger; private readonly IServerProxy server; - private readonly IText text; - private readonly IUserInterfaceFactory uiFactory; - private string filePath; - private ProctoringControl control; - private ProctoringSettings settings; - private IProctoringWindow window; - private WindowVisibility windowVisibility; + private IEnumerable implementations; - public IconResource IconResource { get; set; } public bool IsHandRaised { get; private set; } - public string Tooltip { get; set; } + public IEnumerable Notifications => new List(implementations); public event ProctoringEventHandler HandLowered; public event ProctoringEventHandler HandRaised; - public event NotificationChangedEventHandler NotificationChanged; public ProctoringController( AppConfig appConfig, @@ -60,51 +44,31 @@ namespace SafeExamBrowser.Proctoring IText text, IUserInterfaceFactory uiFactory) { - this.appConfig = appConfig; - this.fileSystem = fileSystem; this.logger = logger; this.server = server; - this.text = text; - this.uiFactory = uiFactory; - IconResource = new XamlIconResource { Uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/ProctoringNotification_Inactive.xaml") }; - Tooltip = text.Get(TextKey.Notification_ProctoringInactiveTooltip); - } - - public void Activate() - { - if (settings.WindowVisibility == WindowVisibility.Visible) - { - window?.BringToForeground(); - } - else if (settings.WindowVisibility == WindowVisibility.AllowToHide || settings.WindowVisibility == WindowVisibility.AllowToShow) - { - window?.Toggle(); - } + factory = new ProctoringFactory(appConfig, fileSystem, logger, text, uiFactory); + implementations = new List(); } public void Initialize(ProctoringSettings settings) { - var start = false; - - this.settings = settings; - this.windowVisibility = settings.WindowVisibility; + implementations = factory.CreateAllActive(settings); server.HandConfirmed += Server_HandConfirmed; server.ProctoringConfigurationReceived += Server_ProctoringConfigurationReceived; server.ProctoringInstructionReceived += Server_ProctoringInstructionReceived; - if (settings.JitsiMeet.Enabled) + foreach (var implementation in implementations) { - this.settings.JitsiMeet.ServerUrl = Sanitize(settings.JitsiMeet.ServerUrl); - - start = !string.IsNullOrWhiteSpace(settings.JitsiMeet.RoomName); - start &= !string.IsNullOrWhiteSpace(settings.JitsiMeet.ServerUrl); - } - - if (start) - { - StartProctoring(); + try + { + implementation.Initialize(); + } + catch (Exception e) + { + logger.Error($"Failed to initialize proctoring implementation '{implementation.Name}'!", e); + } } } @@ -116,6 +80,7 @@ namespace SafeExamBrowser.Proctoring { IsHandRaised = false; HandLowered?.Invoke(); + logger.Info("Hand lowered."); } else @@ -132,6 +97,7 @@ namespace SafeExamBrowser.Proctoring { IsHandRaised = true; HandRaised?.Invoke(); + logger.Info("Hand raised."); } else @@ -142,7 +108,17 @@ namespace SafeExamBrowser.Proctoring public void Terminate() { - StopProctoring(); + foreach (var implementation in implementations) + { + try + { + implementation.Terminate(); + } + catch (Exception e) + { + logger.Error($"Failed to terminate proctoring implementation '{implementation.Name}'!", e); + } + } } private void Server_HandConfirmed() @@ -153,141 +129,34 @@ namespace SafeExamBrowser.Proctoring HandLowered?.Invoke(); } - private void Server_ProctoringInstructionReceived(ProctoringInstructionEventArgs args) - { - logger.Info("Proctoring instruction received."); - - settings.JitsiMeet.RoomName = args.JitsiMeetRoomName; - settings.JitsiMeet.ServerUrl = args.JitsiMeetServerUrl; - settings.JitsiMeet.Token = args.JitsiMeetToken; - - StopProctoring(); - StartProctoring(); - } - private void Server_ProctoringConfigurationReceived(bool allowChat, bool receiveAudio, bool receiveVideo) { - logger.Info("Proctoring configuration received."); - - settings.JitsiMeet.AllowChat = allowChat; - settings.JitsiMeet.ReceiveAudio = receiveAudio; - settings.JitsiMeet.ReceiveVideo = receiveVideo; - - if (allowChat || receiveVideo) - { - settings.WindowVisibility = WindowVisibility.AllowToHide; - } - else - { - settings.WindowVisibility = windowVisibility; - } - - StopProctoring(); - StartProctoring(); - } - - private void StartProctoring() - { - Application.Current.Dispatcher.Invoke(() => + foreach (var implementation in implementations) { try { - var content = LoadContent(settings); - - filePath = Path.Combine(appConfig.TemporaryDirectory, $"{Path.GetRandomFileName()}_index.html"); - fileSystem.Save(content, filePath); - - control = new ProctoringControl(logger.CloneFor(nameof(ProctoringControl)), settings); - control.CreationProperties = new CoreWebView2CreationProperties { UserDataFolder = appConfig.TemporaryDirectory }; - control.EnsureCoreWebView2Async().ContinueWith(_ => - { - control.Dispatcher.Invoke(() => - { - control.CoreWebView2.Navigate(filePath); - }); - }); - - window = uiFactory.CreateProctoringWindow(control); - window.SetTitle(settings.JitsiMeet.Enabled ? settings.JitsiMeet.Subject : ""); - window.Show(); - - if (settings.WindowVisibility == WindowVisibility.AllowToShow || settings.WindowVisibility == WindowVisibility.Hidden) - { - window.Hide(); - } - - IconResource = new XamlIconResource { Uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/ProctoringNotification_Active.xaml") }; - Tooltip = text.Get(TextKey.Notification_ProctoringActiveTooltip); - NotificationChanged?.Invoke(); - - logger.Info($"Started proctoring with {(settings.JitsiMeet.Enabled ? "Jitsi Meet" : "")}."); + implementation.ProctoringConfigurationReceived(allowChat, receiveAudio, receiveVideo); } catch (Exception e) { - logger.Error($"Failed to start proctoring! Reason: {e.Message}", e); - } - }); - } - - private void StopProctoring() - { - if (control != default(ProctoringControl) && window != default(IProctoringWindow)) - { - control.Dispatcher.Invoke(() => - { - if (settings.JitsiMeet.Enabled) - { - control.ExecuteScriptAsync("api.executeCommand('hangup'); api.dispose();"); - } - - Thread.Sleep(2000); - - window.Close(); - control = default; - window = default; - fileSystem.Delete(filePath); - - logger.Info("Stopped proctoring."); - }); - } - } - - private string LoadContent(ProctoringSettings settings) - { - if (settings.JitsiMeet.Enabled) - { - var assembly = Assembly.GetAssembly(typeof(ProctoringController)); - var path = $"{typeof(ProctoringController).Namespace}.JitsiMeet.index.html"; - - using (var stream = assembly.GetManifestResourceStream(path)) - using (var reader = new StreamReader(stream)) - { - var html = reader.ReadToEnd(); - - if (settings.JitsiMeet.Enabled) - { - html = html.Replace("%%_ALLOW_CHAT_%%", settings.JitsiMeet.AllowChat ? "chat" : ""); - html = html.Replace("%%_ALLOW_CLOSED_CAPTIONS_%%", settings.JitsiMeet.AllowClosedCaptions ? "closedcaptions" : ""); - html = html.Replace("%%_ALLOW_RAISE_HAND_%%", settings.JitsiMeet.AllowRaiseHand ? "raisehand" : ""); - html = html.Replace("%%_ALLOW_RECORDING_%%", settings.JitsiMeet.AllowRecording ? "recording" : ""); - html = html.Replace("%%_ALLOW_TILE_VIEW", settings.JitsiMeet.AllowTileView ? "tileview" : ""); - html = html.Replace("'%_AUDIO_MUTED_%'", settings.JitsiMeet.AudioMuted && settings.WindowVisibility != WindowVisibility.Hidden ? "true" : "false"); - html = html.Replace("'%_AUDIO_ONLY_%'", settings.JitsiMeet.AudioOnly ? "true" : "false"); - html = html.Replace("'%_VIDEO_MUTED_%'", settings.JitsiMeet.VideoMuted && settings.WindowVisibility != WindowVisibility.Hidden ? "true" : "false"); - } - - return html; + logger.Error($"Failed to update proctoring configuration for '{implementation.Name}'!", e); } } - else - { - return ""; - } } - private string Sanitize(string serverUrl) + private void Server_ProctoringInstructionReceived(ProctoringInstructionEventArgs args) { - return serverUrl?.Replace($"{Uri.UriSchemeHttp}{Uri.SchemeDelimiter}", "").Replace($"{Uri.UriSchemeHttps}{Uri.SchemeDelimiter}", ""); + foreach (var implementation in implementations) + { + try + { + implementation.ProctoringInstructionReceived(args); + } + catch (Exception e) + { + logger.Error($"Failed to process proctoring instruction for '{implementation.Name}'!", e); + } + } } } } diff --git a/SafeExamBrowser.Proctoring/ProctoringFactory.cs b/SafeExamBrowser.Proctoring/ProctoringFactory.cs new file mode 100644 index 00000000..844e4bdd --- /dev/null +++ b/SafeExamBrowser.Proctoring/ProctoringFactory.cs @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 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.Collections.Generic; +using SafeExamBrowser.Configuration.Contracts; +using SafeExamBrowser.I18n.Contracts; +using SafeExamBrowser.Logging.Contracts; +using SafeExamBrowser.Proctoring.JitsiMeet; +using SafeExamBrowser.Settings.Proctoring; +using SafeExamBrowser.SystemComponents.Contracts; +using SafeExamBrowser.UserInterface.Contracts; + +namespace SafeExamBrowser.Proctoring +{ + internal class ProctoringFactory + { + private readonly AppConfig appConfig; + private readonly IFileSystem fileSystem; + private readonly IModuleLogger logger; + private readonly IText text; + private readonly IUserInterfaceFactory uiFactory; + + public ProctoringFactory(AppConfig appConfig, IFileSystem fileSystem, IModuleLogger logger, IText text, IUserInterfaceFactory uiFactory) + { + this.appConfig = appConfig; + this.fileSystem = fileSystem; + this.logger = logger; + this.text = text; + this.uiFactory = uiFactory; + } + + internal IEnumerable CreateAllActive(ProctoringSettings settings) + { + var implementations = new List(); + + if (settings.JitsiMeet.Enabled) + { + implementations.Add(new JitsiMeetImplementation(appConfig, fileSystem, logger.CloneFor(nameof(JitsiMeet)), settings, text, uiFactory)); + } + + return implementations; + } + } +} diff --git a/SafeExamBrowser.Proctoring/ProctoringImplementation.cs b/SafeExamBrowser.Proctoring/ProctoringImplementation.cs new file mode 100644 index 00000000..b77f0806 --- /dev/null +++ b/SafeExamBrowser.Proctoring/ProctoringImplementation.cs @@ -0,0 +1,34 @@ +/* +* Copyright (c) 2022 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.Core.Contracts.Notifications; +using SafeExamBrowser.Core.Contracts.Notifications.Events; +using SafeExamBrowser.Core.Contracts.Resources.Icons; +using SafeExamBrowser.Server.Contracts.Events; + +namespace SafeExamBrowser.Proctoring +{ + internal abstract class ProctoringImplementation : INotification + { + internal abstract string Name { get; } + + public abstract string Tooltip { get; protected set; } + public abstract IconResource IconResource { get; protected set; } + + public abstract event NotificationChangedEventHandler NotificationChanged; + + public abstract void Activate(); + public abstract void Terminate(); + + internal abstract void Initialize(); + internal abstract void ProctoringConfigurationReceived(bool allowChat, bool receiveAudio, bool receiveVideo); + internal abstract void ProctoringInstructionReceived(ProctoringInstructionEventArgs args); + internal abstract void StartProctoring(); + internal abstract void StopProctoring(); + } +} diff --git a/SafeExamBrowser.Proctoring/SafeExamBrowser.Proctoring.csproj b/SafeExamBrowser.Proctoring/SafeExamBrowser.Proctoring.csproj index a50134db..90bcc7d2 100644 --- a/SafeExamBrowser.Proctoring/SafeExamBrowser.Proctoring.csproj +++ b/SafeExamBrowser.Proctoring/SafeExamBrowser.Proctoring.csproj @@ -73,8 +73,11 @@ + + +