SEBWIN-834: Revised proctoring architecture to allow for simultaneous activation of different implementations.
This commit is contained in:
parent
73fefad434
commit
de5691cb25
9 changed files with 415 additions and 197 deletions
|
@ -27,7 +27,8 @@ namespace SafeExamBrowser.Client.UnitTests.Operations
|
||||||
private ClientContext context;
|
private ClientContext context;
|
||||||
private Mock<IProctoringController> controller;
|
private Mock<IProctoringController> controller;
|
||||||
private Mock<ILogger> logger;
|
private Mock<ILogger> logger;
|
||||||
private Mock<INotification> notification;
|
private Mock<INotification> notification1;
|
||||||
|
private Mock<INotification> notification2;
|
||||||
private AppSettings settings;
|
private AppSettings settings;
|
||||||
private Mock<ITaskbar> taskbar;
|
private Mock<ITaskbar> taskbar;
|
||||||
private Mock<IUserInterfaceFactory> uiFactory;
|
private Mock<IUserInterfaceFactory> uiFactory;
|
||||||
|
@ -41,13 +42,15 @@ namespace SafeExamBrowser.Client.UnitTests.Operations
|
||||||
context = new ClientContext();
|
context = new ClientContext();
|
||||||
controller = new Mock<IProctoringController>();
|
controller = new Mock<IProctoringController>();
|
||||||
logger = new Mock<ILogger>();
|
logger = new Mock<ILogger>();
|
||||||
notification = new Mock<INotification>();
|
notification1 = new Mock<INotification>();
|
||||||
|
notification2 = new Mock<INotification>();
|
||||||
settings = new AppSettings();
|
settings = new AppSettings();
|
||||||
taskbar = new Mock<ITaskbar>();
|
taskbar = new Mock<ITaskbar>();
|
||||||
uiFactory = new Mock<IUserInterfaceFactory>();
|
uiFactory = new Mock<IUserInterfaceFactory>();
|
||||||
|
|
||||||
context.Settings = settings;
|
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]
|
[TestMethod]
|
||||||
|
@ -58,12 +61,13 @@ namespace SafeExamBrowser.Client.UnitTests.Operations
|
||||||
|
|
||||||
Assert.AreEqual(OperationResult.Success, sut.Perform());
|
Assert.AreEqual(OperationResult.Success, sut.Perform());
|
||||||
|
|
||||||
actionCenter.Verify(a => a.AddNotificationControl(It.IsAny<INotificationControl>()), Times.Once);
|
actionCenter.Verify(a => a.AddNotificationControl(It.IsAny<INotificationControl>()), Times.Exactly(2));
|
||||||
controller.Verify(c => c.Initialize(It.Is<ProctoringSettings>(s => s == settings.Proctoring)));
|
controller.Verify(c => c.Initialize(It.Is<ProctoringSettings>(s => s == settings.Proctoring)));
|
||||||
notification.VerifyNoOtherCalls();
|
notification1.VerifyNoOtherCalls();
|
||||||
taskbar.Verify(t => t.AddNotificationControl(It.IsAny<INotificationControl>()), Times.Once);
|
notification2.VerifyNoOtherCalls();
|
||||||
uiFactory.Verify(u => u.CreateNotificationControl(It.Is<INotification>(n => n == notification.Object), Location.ActionCenter), Times.Once);
|
taskbar.Verify(t => t.AddNotificationControl(It.IsAny<INotificationControl>()), Times.Exactly(2));
|
||||||
uiFactory.Verify(u => u.CreateNotificationControl(It.Is<INotification>(n => n == notification.Object), Location.Taskbar), Times.Once);
|
uiFactory.Verify(u => u.CreateNotificationControl(It.IsAny<INotification>(), Location.ActionCenter), Times.Exactly(2));
|
||||||
|
uiFactory.Verify(u => u.CreateNotificationControl(It.IsAny<INotification>(), Location.Taskbar), Times.Exactly(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
|
@ -75,7 +79,8 @@ namespace SafeExamBrowser.Client.UnitTests.Operations
|
||||||
|
|
||||||
actionCenter.VerifyNoOtherCalls();
|
actionCenter.VerifyNoOtherCalls();
|
||||||
controller.VerifyNoOtherCalls();
|
controller.VerifyNoOtherCalls();
|
||||||
notification.VerifyNoOtherCalls();
|
notification1.VerifyNoOtherCalls();
|
||||||
|
notification2.VerifyNoOtherCalls();
|
||||||
taskbar.VerifyNoOtherCalls();
|
taskbar.VerifyNoOtherCalls();
|
||||||
uiFactory.VerifyNoOtherCalls();
|
uiFactory.VerifyNoOtherCalls();
|
||||||
}
|
}
|
||||||
|
@ -89,7 +94,8 @@ namespace SafeExamBrowser.Client.UnitTests.Operations
|
||||||
|
|
||||||
actionCenter.VerifyNoOtherCalls();
|
actionCenter.VerifyNoOtherCalls();
|
||||||
controller.Verify(c => c.Terminate(), Times.Once);
|
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();
|
taskbar.VerifyNoOtherCalls();
|
||||||
uiFactory.VerifyNoOtherCalls();
|
uiFactory.VerifyNoOtherCalls();
|
||||||
}
|
}
|
||||||
|
@ -103,7 +109,8 @@ namespace SafeExamBrowser.Client.UnitTests.Operations
|
||||||
|
|
||||||
actionCenter.VerifyNoOtherCalls();
|
actionCenter.VerifyNoOtherCalls();
|
||||||
controller.VerifyNoOtherCalls();
|
controller.VerifyNoOtherCalls();
|
||||||
notification.VerifyNoOtherCalls();
|
notification1.VerifyNoOtherCalls();
|
||||||
|
notification2.VerifyNoOtherCalls();
|
||||||
taskbar.VerifyNoOtherCalls();
|
taskbar.VerifyNoOtherCalls();
|
||||||
uiFactory.VerifyNoOtherCalls();
|
uiFactory.VerifyNoOtherCalls();
|
||||||
}
|
}
|
||||||
|
|
|
@ -289,7 +289,7 @@ namespace SafeExamBrowser.Client
|
||||||
private IOperation BuildProctoringOperation()
|
private IOperation BuildProctoringOperation()
|
||||||
{
|
{
|
||||||
var controller = new ProctoringController(context.AppConfig, new FileSystem(), ModuleLogger(nameof(ProctoringController)), context.Server, text, uiFactory);
|
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;
|
return operation;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
* 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;
|
||||||
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
|
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
|
||||||
using SafeExamBrowser.I18n.Contracts;
|
using SafeExamBrowser.I18n.Contracts;
|
||||||
|
@ -23,7 +22,6 @@ namespace SafeExamBrowser.Client.Operations
|
||||||
private readonly IActionCenter actionCenter;
|
private readonly IActionCenter actionCenter;
|
||||||
private readonly IProctoringController controller;
|
private readonly IProctoringController controller;
|
||||||
private readonly ILogger logger;
|
private readonly ILogger logger;
|
||||||
private readonly INotification notification;
|
|
||||||
private readonly ITaskbar taskbar;
|
private readonly ITaskbar taskbar;
|
||||||
private readonly IUserInterfaceFactory uiFactory;
|
private readonly IUserInterfaceFactory uiFactory;
|
||||||
|
|
||||||
|
@ -35,14 +33,12 @@ namespace SafeExamBrowser.Client.Operations
|
||||||
ClientContext context,
|
ClientContext context,
|
||||||
IProctoringController controller,
|
IProctoringController controller,
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
INotification notification,
|
|
||||||
ITaskbar taskbar,
|
ITaskbar taskbar,
|
||||||
IUserInterfaceFactory uiFactory) : base(context)
|
IUserInterfaceFactory uiFactory) : base(context)
|
||||||
{
|
{
|
||||||
this.actionCenter = actionCenter;
|
this.actionCenter = actionCenter;
|
||||||
this.controller = controller;
|
this.controller = controller;
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.notification = notification;
|
|
||||||
this.taskbar = taskbar;
|
this.taskbar = taskbar;
|
||||||
this.uiFactory = uiFactory;
|
this.uiFactory = uiFactory;
|
||||||
}
|
}
|
||||||
|
@ -55,7 +51,6 @@ namespace SafeExamBrowser.Client.Operations
|
||||||
StatusChanged?.Invoke(TextKey.OperationStatus_InitializeProctoring);
|
StatusChanged?.Invoke(TextKey.OperationStatus_InitializeProctoring);
|
||||||
|
|
||||||
controller.Initialize(Context.Settings.Proctoring);
|
controller.Initialize(Context.Settings.Proctoring);
|
||||||
actionCenter.AddNotificationControl(uiFactory.CreateNotificationControl(notification, Location.ActionCenter));
|
|
||||||
|
|
||||||
if (Context.Settings.SessionMode == SessionMode.Server && Context.Settings.Proctoring.ShowRaiseHandNotification)
|
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));
|
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);
|
StatusChanged?.Invoke(TextKey.OperationStatus_TerminateProctoring);
|
||||||
|
|
||||||
controller.Terminate();
|
controller.Terminate();
|
||||||
notification.Terminate();
|
|
||||||
|
foreach (var notification in controller.Notifications)
|
||||||
|
{
|
||||||
|
notification.Terminate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return OperationResult.Success;
|
return OperationResult.Success;
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
* 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.Proctoring.Contracts.Events;
|
||||||
using SafeExamBrowser.Settings.Proctoring;
|
using SafeExamBrowser.Settings.Proctoring;
|
||||||
|
|
||||||
|
@ -21,6 +23,11 @@ namespace SafeExamBrowser.Proctoring.Contracts
|
||||||
/// </summary>
|
/// </summary>
|
||||||
bool IsHandRaised { get; }
|
bool IsHandRaised { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The notifications for all active proctoring providers.
|
||||||
|
/// </summary>
|
||||||
|
IEnumerable<INotification> Notifications { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fired when the hand has been lowered.
|
/// Fired when the hand has been lowered.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -44,7 +51,7 @@ namespace SafeExamBrowser.Proctoring.Contracts
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Raises the hand, optionally with the given message.
|
/// Raises the hand, optionally with the given message.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void RaiseHand(string message = default(string));
|
void RaiseHand(string message = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stops the proctoring functionality.
|
/// Stops the proctoring functionality.
|
||||||
|
|
245
SafeExamBrowser.Proctoring/JitsiMeet/JitsiMeetImplementation.cs
Normal file
245
SafeExamBrowser.Proctoring/JitsiMeet/JitsiMeetImplementation.cs
Normal file
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,15 +7,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.Collections.Generic;
|
||||||
using System.Reflection;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Windows;
|
|
||||||
using Microsoft.Web.WebView2.Wpf;
|
|
||||||
using SafeExamBrowser.Configuration.Contracts;
|
using SafeExamBrowser.Configuration.Contracts;
|
||||||
using SafeExamBrowser.Core.Contracts.Notifications;
|
using SafeExamBrowser.Core.Contracts.Notifications;
|
||||||
using SafeExamBrowser.Core.Contracts.Notifications.Events;
|
|
||||||
using SafeExamBrowser.Core.Contracts.Resources.Icons;
|
|
||||||
using SafeExamBrowser.I18n.Contracts;
|
using SafeExamBrowser.I18n.Contracts;
|
||||||
using SafeExamBrowser.Logging.Contracts;
|
using SafeExamBrowser.Logging.Contracts;
|
||||||
using SafeExamBrowser.Proctoring.Contracts;
|
using SafeExamBrowser.Proctoring.Contracts;
|
||||||
|
@ -25,32 +19,22 @@ using SafeExamBrowser.Server.Contracts.Events;
|
||||||
using SafeExamBrowser.Settings.Proctoring;
|
using SafeExamBrowser.Settings.Proctoring;
|
||||||
using SafeExamBrowser.SystemComponents.Contracts;
|
using SafeExamBrowser.SystemComponents.Contracts;
|
||||||
using SafeExamBrowser.UserInterface.Contracts;
|
using SafeExamBrowser.UserInterface.Contracts;
|
||||||
using SafeExamBrowser.UserInterface.Contracts.Proctoring;
|
|
||||||
|
|
||||||
namespace SafeExamBrowser.Proctoring
|
namespace SafeExamBrowser.Proctoring
|
||||||
{
|
{
|
||||||
public class ProctoringController : IProctoringController, INotification
|
public class ProctoringController : IProctoringController
|
||||||
{
|
{
|
||||||
private readonly AppConfig appConfig;
|
private readonly ProctoringFactory factory;
|
||||||
private readonly IFileSystem fileSystem;
|
|
||||||
private readonly IModuleLogger logger;
|
private readonly IModuleLogger logger;
|
||||||
private readonly IServerProxy server;
|
private readonly IServerProxy server;
|
||||||
private readonly IText text;
|
|
||||||
private readonly IUserInterfaceFactory uiFactory;
|
|
||||||
|
|
||||||
private string filePath;
|
private IEnumerable<ProctoringImplementation> implementations;
|
||||||
private ProctoringControl control;
|
|
||||||
private ProctoringSettings settings;
|
|
||||||
private IProctoringWindow window;
|
|
||||||
private WindowVisibility windowVisibility;
|
|
||||||
|
|
||||||
public IconResource IconResource { get; set; }
|
|
||||||
public bool IsHandRaised { get; private set; }
|
public bool IsHandRaised { get; private set; }
|
||||||
public string Tooltip { get; set; }
|
public IEnumerable<INotification> Notifications => new List<INotification>(implementations);
|
||||||
|
|
||||||
public event ProctoringEventHandler HandLowered;
|
public event ProctoringEventHandler HandLowered;
|
||||||
public event ProctoringEventHandler HandRaised;
|
public event ProctoringEventHandler HandRaised;
|
||||||
public event NotificationChangedEventHandler NotificationChanged;
|
|
||||||
|
|
||||||
public ProctoringController(
|
public ProctoringController(
|
||||||
AppConfig appConfig,
|
AppConfig appConfig,
|
||||||
|
@ -60,51 +44,31 @@ namespace SafeExamBrowser.Proctoring
|
||||||
IText text,
|
IText text,
|
||||||
IUserInterfaceFactory uiFactory)
|
IUserInterfaceFactory uiFactory)
|
||||||
{
|
{
|
||||||
this.appConfig = appConfig;
|
|
||||||
this.fileSystem = fileSystem;
|
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.server = server;
|
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") };
|
factory = new ProctoringFactory(appConfig, fileSystem, logger, text, uiFactory);
|
||||||
Tooltip = text.Get(TextKey.Notification_ProctoringInactiveTooltip);
|
implementations = new List<ProctoringImplementation>();
|
||||||
}
|
|
||||||
|
|
||||||
public void Activate()
|
|
||||||
{
|
|
||||||
if (settings.WindowVisibility == WindowVisibility.Visible)
|
|
||||||
{
|
|
||||||
window?.BringToForeground();
|
|
||||||
}
|
|
||||||
else if (settings.WindowVisibility == WindowVisibility.AllowToHide || settings.WindowVisibility == WindowVisibility.AllowToShow)
|
|
||||||
{
|
|
||||||
window?.Toggle();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Initialize(ProctoringSettings settings)
|
public void Initialize(ProctoringSettings settings)
|
||||||
{
|
{
|
||||||
var start = false;
|
implementations = factory.CreateAllActive(settings);
|
||||||
|
|
||||||
this.settings = settings;
|
|
||||||
this.windowVisibility = settings.WindowVisibility;
|
|
||||||
|
|
||||||
server.HandConfirmed += Server_HandConfirmed;
|
server.HandConfirmed += Server_HandConfirmed;
|
||||||
server.ProctoringConfigurationReceived += Server_ProctoringConfigurationReceived;
|
server.ProctoringConfigurationReceived += Server_ProctoringConfigurationReceived;
|
||||||
server.ProctoringInstructionReceived += Server_ProctoringInstructionReceived;
|
server.ProctoringInstructionReceived += Server_ProctoringInstructionReceived;
|
||||||
|
|
||||||
if (settings.JitsiMeet.Enabled)
|
foreach (var implementation in implementations)
|
||||||
{
|
{
|
||||||
this.settings.JitsiMeet.ServerUrl = Sanitize(settings.JitsiMeet.ServerUrl);
|
try
|
||||||
|
{
|
||||||
start = !string.IsNullOrWhiteSpace(settings.JitsiMeet.RoomName);
|
implementation.Initialize();
|
||||||
start &= !string.IsNullOrWhiteSpace(settings.JitsiMeet.ServerUrl);
|
}
|
||||||
}
|
catch (Exception e)
|
||||||
|
{
|
||||||
if (start)
|
logger.Error($"Failed to initialize proctoring implementation '{implementation.Name}'!", e);
|
||||||
{
|
}
|
||||||
StartProctoring();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,6 +80,7 @@ namespace SafeExamBrowser.Proctoring
|
||||||
{
|
{
|
||||||
IsHandRaised = false;
|
IsHandRaised = false;
|
||||||
HandLowered?.Invoke();
|
HandLowered?.Invoke();
|
||||||
|
|
||||||
logger.Info("Hand lowered.");
|
logger.Info("Hand lowered.");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -132,6 +97,7 @@ namespace SafeExamBrowser.Proctoring
|
||||||
{
|
{
|
||||||
IsHandRaised = true;
|
IsHandRaised = true;
|
||||||
HandRaised?.Invoke();
|
HandRaised?.Invoke();
|
||||||
|
|
||||||
logger.Info("Hand raised.");
|
logger.Info("Hand raised.");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -142,7 +108,17 @@ namespace SafeExamBrowser.Proctoring
|
||||||
|
|
||||||
public void Terminate()
|
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()
|
private void Server_HandConfirmed()
|
||||||
|
@ -153,141 +129,34 @@ namespace SafeExamBrowser.Proctoring
|
||||||
HandLowered?.Invoke();
|
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)
|
private void Server_ProctoringConfigurationReceived(bool allowChat, bool receiveAudio, bool receiveVideo)
|
||||||
{
|
{
|
||||||
logger.Info("Proctoring configuration received.");
|
foreach (var implementation in implementations)
|
||||||
|
|
||||||
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(() =>
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var content = LoadContent(settings);
|
implementation.ProctoringConfigurationReceived(allowChat, receiveAudio, receiveVideo);
|
||||||
|
|
||||||
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" : "")}.");
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
logger.Error($"Failed to start proctoring! Reason: {e.Message}", e);
|
logger.Error($"Failed to update proctoring configuration for '{implementation.Name}'!", 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
49
SafeExamBrowser.Proctoring/ProctoringFactory.cs
Normal file
49
SafeExamBrowser.Proctoring/ProctoringFactory.cs
Normal file
|
@ -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<ProctoringImplementation> CreateAllActive(ProctoringSettings settings)
|
||||||
|
{
|
||||||
|
var implementations = new List<ProctoringImplementation>();
|
||||||
|
|
||||||
|
if (settings.JitsiMeet.Enabled)
|
||||||
|
{
|
||||||
|
implementations.Add(new JitsiMeetImplementation(appConfig, fileSystem, logger.CloneFor(nameof(JitsiMeet)), settings, text, uiFactory));
|
||||||
|
}
|
||||||
|
|
||||||
|
return implementations;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
SafeExamBrowser.Proctoring/ProctoringImplementation.cs
Normal file
34
SafeExamBrowser.Proctoring/ProctoringImplementation.cs
Normal file
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -73,8 +73,11 @@
|
||||||
<Reference Include="WindowsBase" />
|
<Reference Include="WindowsBase" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Compile Include="JitsiMeet\JitsiMeetImplementation.cs" />
|
||||||
<Compile Include="ProctoringControl.cs" />
|
<Compile Include="ProctoringControl.cs" />
|
||||||
<Compile Include="ProctoringController.cs" />
|
<Compile Include="ProctoringController.cs" />
|
||||||
|
<Compile Include="ProctoringFactory.cs" />
|
||||||
|
<Compile Include="ProctoringImplementation.cs" />
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
Loading…
Reference in a new issue