diff --git a/SafeExamBrowser.Client/ClientContext.cs b/SafeExamBrowser.Client/ClientContext.cs
index 0984543c..96f2e2ea 100644
--- a/SafeExamBrowser.Client/ClientContext.cs
+++ b/SafeExamBrowser.Client/ClientContext.cs
@@ -13,6 +13,7 @@ using SafeExamBrowser.Browser.Contracts;
using SafeExamBrowser.Communication.Contracts.Hosts;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Configuration.Contracts.Integrity;
+using SafeExamBrowser.Proctoring.Contracts;
using SafeExamBrowser.Server.Contracts;
using SafeExamBrowser.Settings;
using SafeExamBrowser.UserInterface.Contracts.Shell;
@@ -54,6 +55,11 @@ namespace SafeExamBrowser.Client
///
internal IIntegrityModule IntegrityModule { get; set; }
+ ///
+ /// The proctoring controller to be used if the current session has proctoring enabled.
+ ///
+ internal IProctoringController Proctoring { get; set; }
+
///
/// The server proxy to be used if the current session mode is .
///
diff --git a/SafeExamBrowser.Client/ClientController.cs b/SafeExamBrowser.Client/ClientController.cs
index 209cd66b..5a325a03 100644
--- a/SafeExamBrowser.Client/ClientController.cs
+++ b/SafeExamBrowser.Client/ClientController.cs
@@ -30,6 +30,8 @@ using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Monitoring.Contracts.Applications;
using SafeExamBrowser.Monitoring.Contracts.Display;
using SafeExamBrowser.Monitoring.Contracts.System;
+using SafeExamBrowser.Proctoring.Contracts;
+using SafeExamBrowser.Proctoring.Contracts.Events;
using SafeExamBrowser.Server.Contracts;
using SafeExamBrowser.Server.Contracts.Data;
using SafeExamBrowser.Settings;
@@ -68,6 +70,7 @@ namespace SafeExamBrowser.Client
private IBrowserApplication Browser => context.Browser;
private IClientHost ClientHost => context.ClientHost;
private IIntegrityModule IntegrityModule => context.IntegrityModule;
+ private IProctoringController Proctoring => context.Proctoring;
private IServerProxy Server => context.Server;
private AppSettings Settings => context.Settings;
@@ -230,16 +233,6 @@ namespace SafeExamBrowser.Client
}
}
- private void Taskbar_LoseFocusRequested(bool forward)
- {
- Browser.Focus(forward);
- }
-
- private void Browser_LoseFocusRequested(bool forward)
- {
- taskbar.Focus(forward);
- }
-
private void DeregisterEvents()
{
actionCenter.QuitButtonClicked -= Shell_QuitButtonClicked;
@@ -249,6 +242,7 @@ namespace SafeExamBrowser.Client
registry.ValueChanged -= Registry_ValueChanged;
runtime.ConnectionLost -= Runtime_ConnectionLost;
systemMonitor.SessionChanged -= SystemMonitor_SessionChanged;
+ taskbar.LoseFocusRequested -= Taskbar_LoseFocusRequested;
taskbar.QuitButtonClicked -= Shell_QuitButtonClicked;
if (Browser != null)
@@ -327,6 +321,29 @@ namespace SafeExamBrowser.Client
}
}
+ private void PrepareShutdown()
+ {
+ FinalizeProctoring();
+ }
+
+ private void FinalizeProctoring()
+ {
+ if (Proctoring != default && Proctoring.HasRemainingWork())
+ {
+ var dialog = uiFactory.CreateProctoringFinalizationDialog();
+ var handler = new RemainingWorkUpdatedEventHandler((args) => dialog.Update(args));
+
+ Task.Run(() =>
+ {
+ Proctoring.RemainingWorkUpdated += handler;
+ Proctoring.ExecuteRemainingWork();
+ Proctoring.RemainingWorkUpdated -= handler;
+ });
+
+ dialog.Show();
+ }
+ }
+
private void ScheduleIntegrityVerification()
{
const int FIVE_MINUTES = 300000;
@@ -510,6 +527,8 @@ namespace SafeExamBrowser.Client
{
if (success)
{
+ PrepareShutdown();
+
var communication = runtime.RequestReconfiguration(filePath, url);
if (communication.Success)
@@ -531,6 +550,11 @@ namespace SafeExamBrowser.Client
}
}
+ private void Browser_LoseFocusRequested(bool forward)
+ {
+ taskbar.Focus(forward);
+ }
+
private void Browser_UserIdentifierDetected(string identifier)
{
if (Settings.SessionMode == SessionMode.Server)
@@ -876,6 +900,11 @@ namespace SafeExamBrowser.Client
}
}
+ private void Taskbar_LoseFocusRequested(bool forward)
+ {
+ Browser.Focus(forward);
+ }
+
private void TerminationActivator_Activated()
{
PauseActivators();
@@ -1023,19 +1052,19 @@ namespace SafeExamBrowser.Client
private bool TryInitiateShutdown()
{
var hasQuitPassword = !string.IsNullOrEmpty(Settings.Security.QuitPasswordHash);
- var requestShutdown = false;
+ var initiateShutdown = false;
var succes = false;
if (hasQuitPassword)
{
- requestShutdown = TryValidateQuitPassword();
+ initiateShutdown = TryValidateQuitPassword();
}
else
{
- requestShutdown = TryConfirmShutdown();
+ initiateShutdown = TryConfirmShutdown();
}
- if (requestShutdown)
+ if (initiateShutdown)
{
succes = TryRequestShutdown();
}
@@ -1084,6 +1113,8 @@ namespace SafeExamBrowser.Client
private bool TryRequestShutdown()
{
+ PrepareShutdown();
+
var communication = runtime.RequestShutdown();
if (!communication.Success)
diff --git a/SafeExamBrowser.Client/CompositionRoot.cs b/SafeExamBrowser.Client/CompositionRoot.cs
index 2797b2cb..34ede80c 100644
--- a/SafeExamBrowser.Client/CompositionRoot.cs
+++ b/SafeExamBrowser.Client/CompositionRoot.cs
@@ -301,6 +301,8 @@ namespace SafeExamBrowser.Client
uiFactory);
var operation = new ProctoringOperation(actionCenter, context, controller, logger, taskbar, uiFactory);
+ context.Proctoring = controller;
+
return operation;
}
diff --git a/SafeExamBrowser.I18n.Contracts/TextKey.cs b/SafeExamBrowser.I18n.Contracts/TextKey.cs
index 2c4fbf76..ab02bcfb 100644
--- a/SafeExamBrowser.I18n.Contracts/TextKey.cs
+++ b/SafeExamBrowser.I18n.Contracts/TextKey.cs
@@ -232,6 +232,14 @@ namespace SafeExamBrowser.I18n.Contracts
PasswordDialog_QuitPasswordRequiredTitle,
PasswordDialog_SettingsPasswordRequired,
PasswordDialog_SettingsPasswordRequiredTitle,
+ // TODO: Translate text for ProctoringFinalizationDialog_... to remaining languages!
+ ProctoringFinalizationDialog_Confirm,
+ ProctoringFinalizationDialog_FailureMessage,
+ ProctoringFinalizationDialog_InfoMessage,
+ ProctoringFinalizationDialog_Status,
+ ProctoringFinalizationDialog_StatusAndTime,
+ ProctoringFinalizationDialog_StatusWaiting,
+ ProctoringFinalizationDialog_Title,
RuntimeWindow_ApplicationRunning,
ServerFailureDialog_Abort,
ServerFailureDialog_Fallback,
diff --git a/SafeExamBrowser.I18n/Data/de.xml b/SafeExamBrowser.I18n/Data/de.xml
index 3f2deb78..9d93f252 100644
--- a/SafeExamBrowser.I18n/Data/de.xml
+++ b/SafeExamBrowser.I18n/Data/de.xml
@@ -654,6 +654,27 @@
Passwort erforderlich
+
+ Bestätigen
+
+
+ Die verbleibenden Operationen konnten nicht abgeschlossen werden, da ein Problem mit dem Netzwerk und/oder dem Bildschirmüberwachungs-Dienst vorliegt. Die zwischengespeicherten Daten befinden sich im folgenden Verzeichnis:
+
+
+ Bitte warten Sie, während die Bildschirmüberwachung ihre restlichen Operationen ausführt. Dies kann eine Weile dauern, je nach Status des Netzwerkes und Bildschirmüberwachungs-Dienstes.
+
+
+ Ausführen der Übertragungsoperation %%_COUNT_%% von %%_TOTAL_%%.
+
+
+ Warten auf die Ausführung der Übertragungsoperation %%_COUNT_%% von %%_TOTAL_%% um %%_TIME_%%...
+
+
+ Warten auf die Wiederaufnahme von %%_COUNT_%% Übertragungsoperationen um %%_TIME_%%...
+
+
+ Finalisierung der Bildschirmüberwachung
+
SEB wird ausgeführt.
diff --git a/SafeExamBrowser.I18n/Data/en.xml b/SafeExamBrowser.I18n/Data/en.xml
index 2cabb2b6..b8f795dc 100644
--- a/SafeExamBrowser.I18n/Data/en.xml
+++ b/SafeExamBrowser.I18n/Data/en.xml
@@ -654,6 +654,27 @@
Password Required
+
+ Confirm
+
+
+ The remaining operations could not be completed as there is a problem with the network and/or screen proctoring service. The cached data can be found in the following directory:
+
+
+ Please wait while the screen procotoring is executing its remaining operations. This may take a while, depending on the network and screen proctoring service status.
+
+
+ Executing transmission operation %%_COUNT_%% of %%_TOTAL_%%.
+
+
+ Waiting to execute transmission operation %%_COUNT_%% of %%_TOTAL_%% at %%_TIME_%%...
+
+
+ Waiting to resume %%_COUNT_%% transmission operations at %%_TIME_%%...
+
+
+ Screen Proctoring Finalization
+
SEB is running.
diff --git a/SafeExamBrowser.Proctoring.Contracts/Events/RemainingWorkUpdatedEventArgs.cs b/SafeExamBrowser.Proctoring.Contracts/Events/RemainingWorkUpdatedEventArgs.cs
new file mode 100644
index 00000000..1259cad9
--- /dev/null
+++ b/SafeExamBrowser.Proctoring.Contracts/Events/RemainingWorkUpdatedEventArgs.cs
@@ -0,0 +1,58 @@
+/*
+ * 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;
+
+namespace SafeExamBrowser.Proctoring.Contracts.Events
+{
+ ///
+ /// The event arguments used for the remaining work updated event fired by the .
+ ///
+ public class RemainingWorkUpdatedEventArgs
+ {
+ ///
+ /// The path of the local cache, if is true.
+ ///
+ public string CachePath { get; set; }
+
+ ///
+ /// Indicates that the execution of the remaining work has failed.
+ ///
+ public bool HasFailed { get; set; }
+
+ ///
+ /// Indicates that the execution of the remaining work has finished.
+ ///
+ public bool IsFinished { get; set; }
+
+ ///
+ /// Indicates that the execution is paused resp. waiting to be resumed.
+ ///
+ public bool IsWaiting { get; set; }
+
+ ///
+ /// The point in time when the next transmission will take place, if available.
+ ///
+ public DateTime? Next { get; set; }
+
+ ///
+ /// The number of already executed work items.
+ ///
+ public int Progress { get; set; }
+
+ ///
+ /// The point in time when the execution will resume, if is true.
+ ///
+ public DateTime Resume { get; set; }
+
+ ///
+ /// The total number of work items to be executed.
+ ///
+ public int Total { get; set; }
+ }
+}
diff --git a/SafeExamBrowser.Proctoring.Contracts/Events/RemainingWorkUpdatedEventHandler.cs b/SafeExamBrowser.Proctoring.Contracts/Events/RemainingWorkUpdatedEventHandler.cs
new file mode 100644
index 00000000..6e5463b0
--- /dev/null
+++ b/SafeExamBrowser.Proctoring.Contracts/Events/RemainingWorkUpdatedEventHandler.cs
@@ -0,0 +1,15 @@
+/*
+ * 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/.
+ */
+
+namespace SafeExamBrowser.Proctoring.Contracts.Events
+{
+ ///
+ /// Event handler used to indicate that the remaining work status has been updated.
+ ///
+ public delegate void RemainingWorkUpdatedEventHandler(RemainingWorkUpdatedEventArgs args);
+}
diff --git a/SafeExamBrowser.Proctoring.Contracts/IProctoringController.cs b/SafeExamBrowser.Proctoring.Contracts/IProctoringController.cs
index 6fdd4f7c..fcc05ddb 100644
--- a/SafeExamBrowser.Proctoring.Contracts/IProctoringController.cs
+++ b/SafeExamBrowser.Proctoring.Contracts/IProctoringController.cs
@@ -38,6 +38,21 @@ namespace SafeExamBrowser.Proctoring.Contracts
///
event ProctoringEventHandler HandRaised;
+ ///
+ /// Event fired when the status of the remaining work has been updated.
+ ///
+ event RemainingWorkUpdatedEventHandler RemainingWorkUpdated;
+
+ ///
+ /// Executes any remaining work like e.g. the transmission of cached screen shots. Make sure to do so before calling .
+ ///
+ void ExecuteRemainingWork();
+
+ ///
+ /// Indicates whether there is any remaining work which needs to be done before the proctoring can be terminated.
+ ///
+ bool HasRemainingWork();
+
///
/// Initializes the given settings and starts the proctoring if the settings are valid.
///
@@ -54,7 +69,7 @@ namespace SafeExamBrowser.Proctoring.Contracts
void RaiseHand(string message = default);
///
- /// Stops the proctoring functionality.
+ /// Stops the proctoring functionality. Make sure to call beforehand if necessary.
///
void Terminate();
}
diff --git a/SafeExamBrowser.Proctoring.Contracts/SafeExamBrowser.Proctoring.Contracts.csproj b/SafeExamBrowser.Proctoring.Contracts/SafeExamBrowser.Proctoring.Contracts.csproj
index 06715f3e..b33d0e30 100644
--- a/SafeExamBrowser.Proctoring.Contracts/SafeExamBrowser.Proctoring.Contracts.csproj
+++ b/SafeExamBrowser.Proctoring.Contracts/SafeExamBrowser.Proctoring.Contracts.csproj
@@ -56,6 +56,8 @@
+
+
diff --git a/SafeExamBrowser.Proctoring/JitsiMeet/JitsiMeetImplementation.cs b/SafeExamBrowser.Proctoring/JitsiMeet/JitsiMeetImplementation.cs
index 4bc3f8f5..303a4ee8 100644
--- a/SafeExamBrowser.Proctoring/JitsiMeet/JitsiMeetImplementation.cs
+++ b/SafeExamBrowser.Proctoring/JitsiMeet/JitsiMeetImplementation.cs
@@ -13,7 +13,6 @@ 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;
@@ -41,8 +40,6 @@ namespace SafeExamBrowser.Proctoring.JitsiMeet
internal override string Name => nameof(JitsiMeet);
- public override event NotificationChangedEventHandler NotificationChanged;
-
internal JitsiMeetImplementation(
AppConfig appConfig,
IFileSystem fileSystem,
@@ -194,7 +191,6 @@ namespace SafeExamBrowser.Proctoring.JitsiMeet
internal override void Terminate()
{
Stop();
- TerminateNotification();
logger.Info("Terminated proctoring.");
}
@@ -210,11 +206,6 @@ namespace SafeExamBrowser.Proctoring.JitsiMeet
}
}
- protected override void TerminateNotification()
- {
- // Nothing to do here for now.
- }
-
private string LoadContent(ProctoringSettings settings)
{
var assembly = Assembly.GetAssembly(typeof(ProctoringController));
@@ -249,7 +240,7 @@ namespace SafeExamBrowser.Proctoring.JitsiMeet
IconResource = new XamlIconResource { Uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/Proctoring_Active.xaml") };
Tooltip = text.Get(TextKey.Notification_ProctoringActiveTooltip);
- NotificationChanged?.Invoke();
+ InvokeNotificationChanged();
}
private void ShowNotificationInactive()
@@ -258,7 +249,7 @@ namespace SafeExamBrowser.Proctoring.JitsiMeet
IconResource = new XamlIconResource { Uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/Proctoring_Inactive.xaml") };
Tooltip = text.Get(TextKey.Notification_ProctoringInactiveTooltip);
- NotificationChanged?.Invoke();
+ InvokeNotificationChanged();
}
}
}
diff --git a/SafeExamBrowser.Proctoring/ProctoringController.cs b/SafeExamBrowser.Proctoring/ProctoringController.cs
index 7b45b66d..3691f5a5 100644
--- a/SafeExamBrowser.Proctoring/ProctoringController.cs
+++ b/SafeExamBrowser.Proctoring/ProctoringController.cs
@@ -8,6 +8,7 @@
using System;
using System.Collections.Generic;
+using KGySoft.CoreLibraries;
using SafeExamBrowser.Browser.Contracts;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Core.Contracts.Notifications;
@@ -38,6 +39,11 @@ namespace SafeExamBrowser.Proctoring
public event ProctoringEventHandler HandLowered;
public event ProctoringEventHandler HandRaised;
+ public event RemainingWorkUpdatedEventHandler RemainingWorkUpdated
+ {
+ add { implementations.ForEach(i => i.RemainingWorkUpdated += value); }
+ remove { implementations.ForEach(i => i.RemainingWorkUpdated -= value); }
+ }
public ProctoringController(
AppConfig appConfig,
@@ -57,6 +63,43 @@ namespace SafeExamBrowser.Proctoring
implementations = new List();
}
+ public void ExecuteRemainingWork()
+ {
+ foreach (var implementation in implementations)
+ {
+ try
+ {
+ implementation.ExecuteRemainingWork();
+ }
+ catch (Exception e)
+ {
+ logger.Error($"Failed to execute remaining work for '{implementation.Name}'!", e);
+ }
+ }
+ }
+
+ public bool HasRemainingWork()
+ {
+ var hasWork = false;
+
+ foreach (var implementation in implementations)
+ {
+ try
+ {
+ if (implementation.HasRemainingWork())
+ {
+ hasWork = true;
+ }
+ }
+ catch (Exception e)
+ {
+ logger.Error($"Failed to check whether has remaining work for '{implementation.Name}'!", e);
+ }
+ }
+
+ return hasWork;
+ }
+
public void Initialize(ProctoringSettings settings)
{
implementations = factory.CreateAllActive(settings);
diff --git a/SafeExamBrowser.Proctoring/ProctoringImplementation.cs b/SafeExamBrowser.Proctoring/ProctoringImplementation.cs
index 48e1e7eb..0dc67e0c 100644
--- a/SafeExamBrowser.Proctoring/ProctoringImplementation.cs
+++ b/SafeExamBrowser.Proctoring/ProctoringImplementation.cs
@@ -9,6 +9,7 @@
using SafeExamBrowser.Core.Contracts.Notifications;
using SafeExamBrowser.Core.Contracts.Notifications.Events;
using SafeExamBrowser.Core.Contracts.Resources.Icons;
+using SafeExamBrowser.Proctoring.Contracts.Events;
using SafeExamBrowser.Server.Contracts.Events.Proctoring;
namespace SafeExamBrowser.Proctoring
@@ -21,7 +22,8 @@ namespace SafeExamBrowser.Proctoring
public string Tooltip { get; protected set; }
public IconResource IconResource { get; protected set; }
- public abstract event NotificationChangedEventHandler NotificationChanged;
+ internal event RemainingWorkUpdatedEventHandler RemainingWorkUpdated;
+ public event NotificationChangedEventHandler NotificationChanged;
void INotification.Activate()
{
@@ -40,7 +42,20 @@ namespace SafeExamBrowser.Proctoring
internal abstract void Stop();
internal abstract void Terminate();
+ internal virtual void ExecuteRemainingWork() { }
+ internal virtual bool HasRemainingWork() => false;
+
protected virtual void ActivateNotification() { }
protected virtual void TerminateNotification() { }
+
+ protected void InvokeNotificationChanged()
+ {
+ NotificationChanged?.Invoke();
+ }
+
+ protected void InvokeRemainingWorkUpdated(RemainingWorkUpdatedEventArgs args)
+ {
+ RemainingWorkUpdated?.Invoke(args);
+ }
}
}
diff --git a/SafeExamBrowser.Proctoring/SafeExamBrowser.Proctoring.csproj b/SafeExamBrowser.Proctoring/SafeExamBrowser.Proctoring.csproj
index 84f4b465..178e3fe6 100644
--- a/SafeExamBrowser.Proctoring/SafeExamBrowser.Proctoring.csproj
+++ b/SafeExamBrowser.Proctoring/SafeExamBrowser.Proctoring.csproj
@@ -93,6 +93,7 @@
+
diff --git a/SafeExamBrowser.Proctoring/ScreenProctoring/Buffer.cs b/SafeExamBrowser.Proctoring/ScreenProctoring/Buffer.cs
new file mode 100644
index 00000000..a8917e86
--- /dev/null
+++ b/SafeExamBrowser.Proctoring/ScreenProctoring/Buffer.cs
@@ -0,0 +1,92 @@
+/*
+ * 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.Collections.Generic;
+using System.Linq;
+using SafeExamBrowser.Logging.Contracts;
+using SafeExamBrowser.Proctoring.ScreenProctoring.Data;
+using SafeExamBrowser.Proctoring.ScreenProctoring.Imaging;
+
+namespace SafeExamBrowser.Proctoring.ScreenProctoring
+{
+ internal class Buffer
+ {
+ private readonly object @lock = new object();
+
+ private readonly List<(MetaData metaData, DateTime schedule, ScreenShot screenShot)> list;
+ private readonly ILogger logger;
+
+ internal int Count
+ {
+ get
+ {
+ lock (@lock)
+ {
+ return list.Count;
+ }
+ }
+ }
+
+ internal Buffer(ILogger logger)
+ {
+ this.list = new List<(MetaData, DateTime, ScreenShot)>();
+ this.logger = logger;
+ }
+
+ internal bool Any()
+ {
+ lock (@lock)
+ {
+ return list.Any();
+ }
+ }
+
+ internal void Dequeue()
+ {
+ lock (@lock)
+ {
+ if (list.Any())
+ {
+ var (_, schedule, screenShot) = list.First();
+
+ list.RemoveAt(0);
+ logger.Debug($"Removed data for '{screenShot.CaptureTime:HH:mm:ss} -> {schedule:HH:mm:ss}', {Count} item(s) remaining.");
+ }
+ }
+ }
+
+ internal void Enqueue(MetaData metaData, DateTime schedule, ScreenShot screenShot)
+ {
+ lock (@lock)
+ {
+ list.Add((metaData, schedule, screenShot));
+ list.Sort((a, b) => DateTime.Compare(a.schedule, b.schedule));
+
+ logger.Debug($"Buffered data for '{screenShot.CaptureTime:HH:mm:ss} -> {schedule:HH:mm:ss}', now holding {Count} item(s).");
+ }
+ }
+
+ internal bool TryPeek(out MetaData metaData, out DateTime schedule, out ScreenShot screenShot)
+ {
+ lock (@lock)
+ {
+ metaData = default;
+ schedule = default;
+ screenShot = default;
+
+ if (list.Any())
+ {
+ (metaData, schedule, screenShot) = list.First();
+ }
+
+ return metaData != default && screenShot != default;
+ }
+ }
+ }
+}
diff --git a/SafeExamBrowser.Proctoring/ScreenProctoring/Cache.cs b/SafeExamBrowser.Proctoring/ScreenProctoring/Cache.cs
index 70f2b316..32b5e2b2 100644
--- a/SafeExamBrowser.Proctoring/ScreenProctoring/Cache.cs
+++ b/SafeExamBrowser.Proctoring/ScreenProctoring/Cache.cs
@@ -7,7 +7,7 @@
*/
using System;
-using System.Collections.Generic;
+using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
@@ -27,20 +27,21 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
private readonly AppConfig appConfig;
private readonly ILogger logger;
- private readonly Queue<(string fileName, int checksum, string hash)> queue;
+ private readonly ConcurrentQueue<(string fileName, int checksum, string hash)> queue;
- private string Directory { get; set; }
+ internal int Count => queue.Count;
+ internal string Directory { get; private set; }
public Cache(AppConfig appConfig, ILogger logger)
{
this.appConfig = appConfig;
this.logger = logger;
- this.queue = new Queue<(string, int, string)>();
+ this.queue = new ConcurrentQueue<(string, int, string)>();
}
internal bool Any()
{
- return false;
+ return queue.Any();
}
internal bool TryEnqueue(MetaData metaData, ScreenShot screenShot)
@@ -57,7 +58,7 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
success = true;
- logger.Debug($"Cached data for '{fileName}'.");
+ logger.Debug($"Cached data for '{fileName}', now holding {Count} item(s).");
}
catch (Exception e)
{
@@ -74,23 +75,21 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
metaData = default;
screenShot = default;
- if (queue.Any())
+ if (queue.Any() && queue.TryPeek(out var item))
{
- var (fileName, checksum, hash) = queue.Peek();
-
try
{
- LoadData(fileName, out metaData, out screenShot);
- LoadImage(fileName, screenShot);
- Dequeue(fileName, checksum, hash, metaData, screenShot);
+ LoadData(item.fileName, out metaData, out screenShot);
+ LoadImage(item.fileName, screenShot);
+ Dequeue(item.fileName, item.checksum, item.hash, metaData, screenShot);
success = true;
- logger.Debug($"Uncached data for '{fileName}'.");
+ logger.Debug($"Removed data for '{item.fileName}', {Count} item(s) remaining.");
}
catch (Exception e)
{
- logger.Error($"Failed to uncache data for '{fileName}'!", e);
+ logger.Error($"Failed to remove data for '{item.fileName}'!", e);
}
}
@@ -116,7 +115,7 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
File.Delete(dataPath);
File.Delete(imagePath);
- queue.Dequeue();
+ while (!queue.TryDequeue(out _)) ;
}
private void Enqueue(string fileName, MetaData metaData, ScreenShot screenShot)
diff --git a/SafeExamBrowser.Proctoring/ScreenProctoring/ScreenProctoringImplementation.cs b/SafeExamBrowser.Proctoring/ScreenProctoring/ScreenProctoringImplementation.cs
index f09e86a1..0a5b7740 100644
--- a/SafeExamBrowser.Proctoring/ScreenProctoring/ScreenProctoringImplementation.cs
+++ b/SafeExamBrowser.Proctoring/ScreenProctoring/ScreenProctoringImplementation.cs
@@ -9,7 +9,6 @@
using System;
using SafeExamBrowser.Browser.Contracts;
using SafeExamBrowser.Configuration.Contracts;
-using SafeExamBrowser.Core.Contracts.Notifications.Events;
using SafeExamBrowser.Core.Contracts.Resources.Icons;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
@@ -34,8 +33,6 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
internal override string Name => nameof(ScreenProctoring);
- public override event NotificationChangedEventHandler NotificationChanged;
-
internal ScreenProctoringImplementation(
AppConfig appConfig,
IApplicationMonitor applicationMonitor,
@@ -54,6 +51,29 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
this.text = text;
}
+ internal override void ExecuteRemainingWork()
+ {
+ logger.Info("Starting execution of remaining work...");
+ spooler.ExecuteRemainingWork(InvokeRemainingWorkUpdated);
+ logger.Info("Terminated execution of remaining work.");
+ }
+
+ internal override bool HasRemainingWork()
+ {
+ var hasWork = spooler.HasRemainingWork();
+
+ if (hasWork)
+ {
+ logger.Info("There is remaining work to be done.");
+ }
+ else
+ {
+ logger.Info("There is no remaining work to be done.");
+ }
+
+ return hasWork;
+ }
+
internal override void Initialize()
{
var start = true;
@@ -132,10 +152,7 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
internal override void Terminate()
{
- // TODO: Cache transmission or user information!
-
Stop();
- TerminateNotification();
logger.Info("Terminated proctoring.");
}
@@ -189,7 +206,7 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
Tooltip = text.Get(TextKey.Notification_ProctoringInactiveTooltip);
}
- NotificationChanged?.Invoke();
+ InvokeNotificationChanged();
}
}
}
diff --git a/SafeExamBrowser.Proctoring/ScreenProctoring/TransmissionSpooler.cs b/SafeExamBrowser.Proctoring/ScreenProctoring/TransmissionSpooler.cs
index 855b5654..e6c58944 100644
--- a/SafeExamBrowser.Proctoring/ScreenProctoring/TransmissionSpooler.cs
+++ b/SafeExamBrowser.Proctoring/ScreenProctoring/TransmissionSpooler.cs
@@ -8,12 +8,11 @@
using System;
using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Linq;
using System.Threading;
using System.Timers;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Logging.Contracts;
+using SafeExamBrowser.Proctoring.Contracts.Events;
using SafeExamBrowser.Proctoring.ScreenProctoring.Data;
using SafeExamBrowser.Proctoring.ScreenProctoring.Imaging;
using SafeExamBrowser.Proctoring.ScreenProctoring.Service;
@@ -23,9 +22,10 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
{
internal class TransmissionSpooler
{
- const int BAD = 10;
- const int GOOD = 0;
+ private const int BAD = 10;
+ private const int GOOD = 0;
+ private readonly Buffer buffer;
private readonly Cache cache;
private readonly ILogger logger;
private readonly ConcurrentQueue<(MetaData metaData, ScreenShot screenShot)> queue;
@@ -33,8 +33,8 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
private readonly ServiceProxy service;
private readonly Timer timer;
- private Queue<(MetaData metaData, DateTime schedule, ScreenShot screenShot)> buffer;
private int health;
+ private bool networkIssue;
private bool recovering;
private DateTime resume;
private Thread thread;
@@ -42,7 +42,7 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
internal TransmissionSpooler(AppConfig appConfig, IModuleLogger logger, ServiceProxy service)
{
- this.buffer = new Queue<(MetaData, DateTime, ScreenShot)>();
+ this.buffer = new Buffer(logger.CloneFor(nameof(Buffer)));
this.cache = new Cache(appConfig, logger.CloneFor(nameof(Cache)));
this.logger = logger;
this.queue = new ConcurrentQueue<(MetaData, ScreenShot)>();
@@ -56,6 +56,55 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
queue.Enqueue((metaData, screenShot));
}
+ internal void ExecuteRemainingWork(Action updateStatus)
+ {
+ var previous = buffer.Count + cache.Count;
+ var progress = 0;
+ var total = previous;
+
+ while (HasRemainingWork() && service.IsConnected && (!networkIssue || recovering))
+ {
+ var remaining = buffer.Count + cache.Count;
+
+ if (total < remaining)
+ {
+ total = remaining;
+ }
+ else if (previous < remaining)
+ {
+ total += remaining - previous;
+ }
+
+ previous = remaining;
+ progress = total - remaining;
+
+ updateStatus(new RemainingWorkUpdatedEventArgs
+ {
+ IsWaiting = recovering,
+ Next = buffer.TryPeek(out _, out var schedule, out _) ? schedule : default(DateTime?),
+ Progress = progress,
+ Resume = resume,
+ Total = total
+ });
+
+ Thread.Sleep(100);
+ }
+
+ if (networkIssue)
+ {
+ updateStatus(new RemainingWorkUpdatedEventArgs { HasFailed = true, CachePath = cache.Directory });
+ }
+ else
+ {
+ updateStatus(new RemainingWorkUpdatedEventArgs { IsFinished = true });
+ }
+ }
+
+ internal bool HasRemainingWork()
+ {
+ return buffer.Any() || cache.Any();
+ }
+
internal void Start()
{
const int FIFTEEN_SECONDS = 15000;
@@ -167,14 +216,14 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
{
BufferFromCache();
BufferFromQueue();
- TryTransmitFromBuffer();
+ TransmitFromBuffer();
}
private void ExecuteNormally()
{
- TryTransmitFromBuffer();
- TryTransmitFromCache();
- TryTransmitFromQueue();
+ TransmitFromBuffer();
+ TransmitFromCache();
+ TransmitFromQueue();
}
private void ExecuteRecovery()
@@ -194,17 +243,11 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
}
}
- private void Buffer(MetaData metaData, DateTime schedule, ScreenShot screenShot)
- {
- buffer.Enqueue((metaData, schedule, screenShot));
- buffer = new Queue<(MetaData, DateTime, ScreenShot)>(buffer.OrderBy((b) => b.schedule));
- }
-
private void BufferFromCache()
{
if (cache.TryDequeue(out var metaData, out var screenShot))
{
- Buffer(metaData, CalculateSchedule(metaData), screenShot);
+ buffer.Enqueue(metaData, CalculateSchedule(metaData), screenShot);
}
}
@@ -212,21 +255,16 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
{
if (TryDequeue(out var metaData, out var screenShot))
{
- Buffer(metaData, CalculateSchedule(metaData), screenShot);
+ buffer.Enqueue(metaData, CalculateSchedule(metaData), screenShot);
}
}
private void CacheFromBuffer()
{
- if (TryPeekFromBuffer(out var metaData, out _, out var screenShot))
+ if (buffer.TryPeek(out var metaData, out _, out var screenShot) && cache.TryEnqueue(metaData, screenShot))
{
- var success = cache.TryEnqueue(metaData, screenShot);
-
- if (success)
- {
- buffer.Dequeue();
- screenShot.Dispose();
- }
+ buffer.Dequeue();
+ screenShot.Dispose();
}
}
@@ -234,15 +272,13 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
{
if (TryDequeue(out var metaData, out var screenShot))
{
- var success = cache.TryEnqueue(metaData, screenShot);
-
- if (success)
+ if (cache.TryEnqueue(metaData, screenShot))
{
screenShot.Dispose();
}
else
{
- Buffer(metaData, DateTime.Now, screenShot);
+ buffer.Enqueue(metaData, DateTime.Now, screenShot);
}
}
}
@@ -255,6 +291,48 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
return schedule;
}
+ private void TransmitFromBuffer()
+ {
+ var hasItem = buffer.TryPeek(out var metaData, out var schedule, out var screenShot);
+ var ready = schedule <= DateTime.Now;
+
+ if (hasItem && ready && TryTransmit(metaData, screenShot))
+ {
+ buffer.Dequeue();
+ screenShot.Dispose();
+ }
+ }
+
+ private void TransmitFromCache()
+ {
+ if (cache.TryDequeue(out var metaData, out var screenShot))
+ {
+ if (TryTransmit(metaData, screenShot))
+ {
+ screenShot.Dispose();
+ }
+ else
+ {
+ buffer.Enqueue(metaData, DateTime.Now, screenShot);
+ }
+ }
+ }
+
+ private void TransmitFromQueue()
+ {
+ if (TryDequeue(out var metaData, out var screenShot))
+ {
+ if (TryTransmit(metaData, screenShot))
+ {
+ screenShot.Dispose();
+ }
+ else
+ {
+ buffer.Enqueue(metaData, DateTime.Now, screenShot);
+ }
+ }
+ }
+
private bool TryDequeue(out MetaData metaData, out ScreenShot screenShot)
{
metaData = default;
@@ -269,80 +347,6 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
return metaData != default && screenShot != default;
}
- private bool TryPeekFromBuffer(out MetaData metaData, out DateTime schedule, out ScreenShot screenShot)
- {
- metaData = default;
- schedule = default;
- screenShot = default;
-
- if (buffer.Any())
- {
- (metaData, schedule, screenShot) = buffer.Peek();
- }
-
- return metaData != default && screenShot != default;
- }
-
- private bool TryTransmitFromBuffer()
- {
- var success = false;
-
- if (TryPeekFromBuffer(out var metaData, out var schedule, out var screenShot) && schedule <= DateTime.Now)
- {
- success = TryTransmit(metaData, screenShot);
-
- if (success)
- {
- buffer.Dequeue();
- screenShot.Dispose();
- }
- }
-
- return success;
- }
-
- private bool TryTransmitFromCache()
- {
- var success = true;
-
- if (cache.TryDequeue(out var metaData, out var screenShot))
- {
- success = TryTransmit(metaData, screenShot);
-
- if (success)
- {
- screenShot.Dispose();
- }
- else
- {
- Buffer(metaData, DateTime.Now, screenShot);
- }
- }
-
- return success;
- }
-
- private bool TryTransmitFromQueue()
- {
- var success = false;
-
- if (TryDequeue(out var metaData, out var screenShot))
- {
- success = TryTransmit(metaData, screenShot);
-
- if (success)
- {
- screenShot.Dispose();
- }
- else
- {
- Buffer(metaData, DateTime.Now, screenShot);
- }
- }
-
- return success;
- }
-
private bool TryTransmit(MetaData metaData, ScreenShot screenShot)
{
var success = false;
@@ -351,10 +355,12 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
{
var response = service.Send(metaData, screenShot);
+ networkIssue = !response.Success;
+ success = response.Success;
+
if (response.Success)
{
health = UpdateHealth(response.Value);
- success = true;
}
}
else
@@ -371,6 +377,8 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
{
var response = service.GetHealth();
+ networkIssue = !response.Success;
+
if (response.Success)
{
health = UpdateHealth(response.Value);
diff --git a/SafeExamBrowser.UserInterface.Contracts/IUserInterfaceFactory.cs b/SafeExamBrowser.UserInterface.Contracts/IUserInterfaceFactory.cs
index 8d41b793..79398ce7 100644
--- a/SafeExamBrowser.UserInterface.Contracts/IUserInterfaceFactory.cs
+++ b/SafeExamBrowser.UserInterface.Contracts/IUserInterfaceFactory.cs
@@ -103,6 +103,11 @@ namespace SafeExamBrowser.UserInterface.Contracts
///
ISystemControl CreatePowerSupplyControl(IPowerSupply powerSupply, Location location);
+ ///
+ /// Creates a new dialog to display the status of the proctoring finalization.
+ ///
+ IProctoringFinalizationDialog CreateProctoringFinalizationDialog();
+
///
/// Creates a new proctoring window loaded with the given proctoring control.
///
diff --git a/SafeExamBrowser.UserInterface.Contracts/Proctoring/IProctoringFinalizationDialog.cs b/SafeExamBrowser.UserInterface.Contracts/Proctoring/IProctoringFinalizationDialog.cs
new file mode 100644
index 00000000..055198f3
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Contracts/Proctoring/IProctoringFinalizationDialog.cs
@@ -0,0 +1,28 @@
+/*
+ * 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 SafeExamBrowser.Proctoring.Contracts.Events;
+
+namespace SafeExamBrowser.UserInterface.Contracts.Proctoring
+{
+ ///
+ /// The dialog to display the status of the proctoring finalization.
+ ///
+ public interface IProctoringFinalizationDialog
+ {
+ ///
+ /// Shows the dialog as topmost window.
+ ///
+ void Show();
+
+ ///
+ /// Updates the status of the finalization.
+ ///
+ void Update(RemainingWorkUpdatedEventArgs status);
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj b/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj
index 726d8327..b75ee7b2 100644
--- a/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj
+++ b/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj
@@ -103,6 +103,7 @@
+
diff --git a/SafeExamBrowser.UserInterface.Desktop/SafeExamBrowser.UserInterface.Desktop.csproj b/SafeExamBrowser.UserInterface.Desktop/SafeExamBrowser.UserInterface.Desktop.csproj
index 558f3db6..9667b599 100644
--- a/SafeExamBrowser.UserInterface.Desktop/SafeExamBrowser.UserInterface.Desktop.csproj
+++ b/SafeExamBrowser.UserInterface.Desktop/SafeExamBrowser.UserInterface.Desktop.csproj
@@ -171,6 +171,9 @@
PasswordDialog.xaml
+
+ ProctoringFinalizationDialog.xaml
+
ProctoringWindow.xaml
@@ -391,6 +394,10 @@
Designer
MSBuild:Compile
+
+ Designer
+ MSBuild:Compile
+
Designer
MSBuild:Compile
diff --git a/SafeExamBrowser.UserInterface.Desktop/UserInterfaceFactory.cs b/SafeExamBrowser.UserInterface.Desktop/UserInterfaceFactory.cs
index 5b0344a4..a8322e45 100644
--- a/SafeExamBrowser.UserInterface.Desktop/UserInterfaceFactory.cs
+++ b/SafeExamBrowser.UserInterface.Desktop/UserInterfaceFactory.cs
@@ -177,6 +177,11 @@ namespace SafeExamBrowser.UserInterface.Desktop
}
}
+ public IProctoringFinalizationDialog CreateProctoringFinalizationDialog()
+ {
+ return Application.Current.Dispatcher.Invoke(() => new ProctoringFinalizationDialog(text));
+ }
+
public IProctoringWindow CreateProctoringWindow(IProctoringControl control)
{
return Application.Current.Dispatcher.Invoke(() => new ProctoringWindow(control));
diff --git a/SafeExamBrowser.UserInterface.Desktop/Windows/ProctoringFinalizationDialog.xaml b/SafeExamBrowser.UserInterface.Desktop/Windows/ProctoringFinalizationDialog.xaml
new file mode 100644
index 00000000..3f75ddc0
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Desktop/Windows/ProctoringFinalizationDialog.xaml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SafeExamBrowser.UserInterface.Desktop/Windows/ProctoringFinalizationDialog.xaml.cs b/SafeExamBrowser.UserInterface.Desktop/Windows/ProctoringFinalizationDialog.xaml.cs
new file mode 100644
index 00000000..2707ce7b
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Desktop/Windows/ProctoringFinalizationDialog.xaml.cs
@@ -0,0 +1,114 @@
+/*
+ * 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.Windows;
+using System.Windows.Input;
+using SafeExamBrowser.I18n.Contracts;
+using SafeExamBrowser.Proctoring.Contracts.Events;
+using SafeExamBrowser.UserInterface.Contracts.Proctoring;
+using SafeExamBrowser.UserInterface.Shared.Utilities;
+
+namespace SafeExamBrowser.UserInterface.Desktop.Windows
+{
+ public partial class ProctoringFinalizationDialog : Window, IProctoringFinalizationDialog
+ {
+ private readonly IText text;
+
+ public ProctoringFinalizationDialog(IText text)
+ {
+ this.text = text;
+
+ InitializeComponent();
+ InitializeDialog();
+ }
+
+ public new void Show()
+ {
+ Dispatcher.Invoke(() => ShowDialog());
+ }
+
+ public void Update(RemainingWorkUpdatedEventArgs status)
+ {
+ Dispatcher.Invoke(() =>
+ {
+ if (status.HasFailed)
+ {
+ ShowFailure(status);
+ }
+ else if (status.IsFinished)
+ {
+ Close();
+ }
+ else
+ {
+ ShowProgress(status);
+ }
+ });
+ }
+
+ private void InitializeDialog()
+ {
+ Button.Click += (o, args) => Close();
+ Button.Content = text.Get(TextKey.ProctoringFinalizationDialog_Confirm);
+ Loaded += (o, args) => this.DisableCloseButton();
+ Title = text.Get(TextKey.ProctoringFinalizationDialog_Title);
+ }
+
+ private void ShowFailure(RemainingWorkUpdatedEventArgs status)
+ {
+ ButtonPanel.Visibility = Visibility.Visible;
+ CachePath.Text = status.CachePath;
+ Cursor = Cursors.Arrow;
+ FailurePanel.Visibility = Visibility.Visible;
+ Message.Text = text.Get(TextKey.ProctoringFinalizationDialog_FailureMessage);
+ ProgressPanel.Visibility = Visibility.Collapsed;
+
+ this.EnableCloseButton();
+ }
+
+ private void ShowProgress(RemainingWorkUpdatedEventArgs status)
+ {
+ ButtonPanel.Visibility = Visibility.Collapsed;
+ Cursor = Cursors.Wait;
+ FailurePanel.Visibility = Visibility.Collapsed;
+ Info.Text = text.Get(TextKey.ProctoringFinalizationDialog_InfoMessage);
+ ProgressPanel.Visibility = Visibility.Visible;
+
+ if (status.IsWaiting)
+ {
+ var count = $"{status.Total - status.Progress}";
+ var time = $"{status.Resume.ToLongTimeString()}";
+
+ Percentage.Text = "";
+ Progress.IsIndeterminate = true;
+ Status.Text = text.Get(TextKey.ProctoringFinalizationDialog_StatusWaiting).Replace("%%_COUNT_%%", count).Replace("%%_TIME_%%", time);
+ }
+ else
+ {
+ var count = $"{status.Progress}";
+ var total = $"{status.Total}";
+
+ Percentage.Text = $"{status.Progress / (double) (status.Total > 0 ? status.Total : 1) * 100:N0}%";
+ Progress.IsIndeterminate = false;
+ Progress.Maximum = status.Total;
+ Progress.Value = status.Progress;
+
+ if (status.Next.HasValue)
+ {
+ Status.Text = text.Get(TextKey.ProctoringFinalizationDialog_StatusAndTime).Replace("%%_TIME_%%", $"{status.Next.Value.ToLongTimeString()}");
+ }
+ else
+ {
+ Status.Text = text.Get(TextKey.ProctoringFinalizationDialog_Status);
+ }
+
+ Status.Text = Status.Text.Replace("%%_COUNT_%%", count).Replace("%%_TOTAL_%%", total);
+ }
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/SafeExamBrowser.UserInterface.Mobile.csproj b/SafeExamBrowser.UserInterface.Mobile/SafeExamBrowser.UserInterface.Mobile.csproj
index 0181ff5e..0179498c 100644
--- a/SafeExamBrowser.UserInterface.Mobile/SafeExamBrowser.UserInterface.Mobile.csproj
+++ b/SafeExamBrowser.UserInterface.Mobile/SafeExamBrowser.UserInterface.Mobile.csproj
@@ -179,6 +179,9 @@
Code
+
+ ProctoringFinalizationDialog.xaml
+
ProctoringWindow.xaml
@@ -522,6 +525,10 @@
MSBuild:Compile
Designer
+
+ Designer
+ MSBuild:Compile
+
MSBuild:Compile
Designer
diff --git a/SafeExamBrowser.UserInterface.Mobile/UserInterfaceFactory.cs b/SafeExamBrowser.UserInterface.Mobile/UserInterfaceFactory.cs
index 05f8d89f..748a1a6b 100644
--- a/SafeExamBrowser.UserInterface.Mobile/UserInterfaceFactory.cs
+++ b/SafeExamBrowser.UserInterface.Mobile/UserInterfaceFactory.cs
@@ -177,6 +177,11 @@ namespace SafeExamBrowser.UserInterface.Mobile
}
}
+ public IProctoringFinalizationDialog CreateProctoringFinalizationDialog()
+ {
+ return Application.Current.Dispatcher.Invoke(() => new ProctoringFinalizationDialog(text));
+ }
+
public IProctoringWindow CreateProctoringWindow(IProctoringControl control)
{
return Application.Current.Dispatcher.Invoke(() => new ProctoringWindow(control));
diff --git a/SafeExamBrowser.UserInterface.Mobile/Windows/ProctoringFinalizationDialog.xaml b/SafeExamBrowser.UserInterface.Mobile/Windows/ProctoringFinalizationDialog.xaml
new file mode 100644
index 00000000..a9440920
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Windows/ProctoringFinalizationDialog.xaml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SafeExamBrowser.UserInterface.Mobile/Windows/ProctoringFinalizationDialog.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/Windows/ProctoringFinalizationDialog.xaml.cs
new file mode 100644
index 00000000..7266a531
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Windows/ProctoringFinalizationDialog.xaml.cs
@@ -0,0 +1,135 @@
+/*
+ * 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.ComponentModel;
+using System.Windows;
+using System.Windows.Input;
+using SafeExamBrowser.I18n.Contracts;
+using SafeExamBrowser.Proctoring.Contracts.Events;
+using SafeExamBrowser.UserInterface.Contracts.Proctoring;
+
+namespace SafeExamBrowser.UserInterface.Mobile.Windows
+{
+ public partial class ProctoringFinalizationDialog : Window, IProctoringFinalizationDialog
+ {
+ private readonly IText text;
+
+ public ProctoringFinalizationDialog(IText text)
+ {
+ this.text = text;
+
+ InitializeComponent();
+ InitializeDialog();
+ }
+
+ public new void Show()
+ {
+ Dispatcher.Invoke(() =>
+ {
+ InitializeBounds();
+ ShowDialog();
+ });
+ }
+
+ public void Update(RemainingWorkUpdatedEventArgs status)
+ {
+ Dispatcher.Invoke(() =>
+ {
+ if (status.HasFailed)
+ {
+ ShowFailure(status);
+ }
+ else if (status.IsFinished)
+ {
+ Close();
+ }
+ else
+ {
+ ShowProgress(status);
+ }
+ });
+ }
+
+ private void InitializeBounds()
+ {
+ Left = 0;
+ Top = 0;
+ Height = SystemParameters.PrimaryScreenHeight;
+ Width = SystemParameters.PrimaryScreenWidth;
+ }
+
+ private void InitializeDialog()
+ {
+ Button.Click += (o, args) => Close();
+ Button.Content = text.Get(TextKey.ProctoringFinalizationDialog_Confirm);
+ Title = text.Get(TextKey.ProctoringFinalizationDialog_Title);
+
+ InitializeBounds();
+
+ SystemParameters.StaticPropertyChanged += SystemParameters_StaticPropertyChanged;
+ }
+
+ private void ShowFailure(RemainingWorkUpdatedEventArgs status)
+ {
+ ButtonPanel.Visibility = Visibility.Visible;
+ CachePath.Text = status.CachePath;
+ Cursor = Cursors.Arrow;
+ FailurePanel.Visibility = Visibility.Visible;
+ Message.Text = text.Get(TextKey.ProctoringFinalizationDialog_FailureMessage);
+ ProgressPanel.Visibility = Visibility.Collapsed;
+ }
+
+ private void ShowProgress(RemainingWorkUpdatedEventArgs status)
+ {
+ ButtonPanel.Visibility = Visibility.Collapsed;
+ Cursor = Cursors.Wait;
+ FailurePanel.Visibility = Visibility.Collapsed;
+ Info.Text = text.Get(TextKey.ProctoringFinalizationDialog_InfoMessage);
+ ProgressPanel.Visibility = Visibility.Visible;
+
+ if (status.IsWaiting)
+ {
+ var count = $"{status.Total - status.Progress}";
+ var time = $"{status.Resume.ToLongTimeString()}";
+
+ Percentage.Text = "";
+ Progress.IsIndeterminate = true;
+ Status.Text = text.Get(TextKey.ProctoringFinalizationDialog_StatusWaiting).Replace("%%_COUNT_%%", count).Replace("%%_TIME_%%", time);
+ }
+ else
+ {
+ var count = $"{status.Progress}";
+ var total = $"{status.Total}";
+
+ Percentage.Text = $"{status.Progress / (double) (status.Total > 0 ? status.Total : 1) * 100:N0}%";
+ Progress.IsIndeterminate = false;
+ Progress.Maximum = status.Total;
+ Progress.Value = status.Progress;
+
+ if (status.Next.HasValue)
+ {
+ Status.Text = text.Get(TextKey.ProctoringFinalizationDialog_StatusAndTime).Replace("%%_TIME_%%", $"{status.Next.Value.ToLongTimeString()}");
+ }
+ else
+ {
+ Status.Text = text.Get(TextKey.ProctoringFinalizationDialog_Status);
+ }
+
+ Status.Text = Status.Text.Replace("%%_COUNT_%%", count).Replace("%%_TOTAL_%%", total);
+ }
+ }
+
+ private void SystemParameters_StaticPropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName == nameof(SystemParameters.WorkArea))
+ {
+ Dispatcher.InvokeAsync(InitializeBounds);
+ }
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Shared/Utilities/WindowExtensions.cs b/SafeExamBrowser.UserInterface.Shared/Utilities/WindowExtensions.cs
index 72b52efd..011ba4e7 100644
--- a/SafeExamBrowser.UserInterface.Shared/Utilities/WindowExtensions.cs
+++ b/SafeExamBrowser.UserInterface.Shared/Utilities/WindowExtensions.cs
@@ -17,8 +17,8 @@ namespace SafeExamBrowser.UserInterface.Shared.Utilities
{
private const int GWL_STYLE = -16;
private const uint MF_BYCOMMAND = 0x00000000;
- private const uint MF_GRAYED = 0x00000001;
private const uint MF_ENABLED = 0x00000000;
+ private const uint MF_GRAYED = 0x00000001;
private const uint SC_CLOSE = 0xF060;
private const uint SWP_SHOWWINDOW = 0x0040;
private const int WS_SYSMENU = 0x80000;
@@ -36,6 +36,17 @@ namespace SafeExamBrowser.UserInterface.Shared.Utilities
}
}
+ public static void EnableCloseButton(this Window window)
+ {
+ var helper = new WindowInteropHelper(window);
+ var systemMenu = GetSystemMenu(helper.Handle, false);
+
+ if (systemMenu != IntPtr.Zero)
+ {
+ EnableMenuItem(systemMenu, SC_CLOSE, MF_BYCOMMAND | MF_ENABLED);
+ }
+ }
+
public static void HideCloseButton(this Window window)
{
var helper = new WindowInteropHelper(window);