Public Access
PR and Push Build/Test / build-and-test (push) Successful in 12m0s
Append successful offline recovery with a duplicate-warning marker and retain failed replay sources. Offer newly available microphones through confirmation notifications. Add project discovery metadata and blocking association approval, plus nonblocking workflow requests that start a separate settings-agent conversation. Include regression tests, OpenSpec changes, and verified local Release startup.
97 lines
3.7 KiB
C#
97 lines
3.7 KiB
C#
#if WINDOWS
|
|
using System.Collections.Concurrent;
|
|
using CommunityToolkit.WinUI.Notifications;
|
|
|
|
namespace MeetingAssistant.Recording;
|
|
|
|
public sealed class WindowsMicrophoneSwitchPromptService : IMicrophoneSwitchPromptService, IDisposable
|
|
{
|
|
private const string Source = "meeting-microphone-switch";
|
|
private readonly ConcurrentDictionary<string, TaskCompletionSource<bool>> pending = new();
|
|
private readonly ILogger<WindowsMicrophoneSwitchPromptService> logger;
|
|
private readonly object gate = new();
|
|
private bool registered;
|
|
|
|
public WindowsMicrophoneSwitchPromptService(ILogger<WindowsMicrophoneSwitchPromptService> logger)
|
|
{
|
|
this.logger = logger;
|
|
}
|
|
|
|
public async Task<bool> ConfirmSwitchAsync(MicrophoneDevice microphone, CancellationToken cancellationToken)
|
|
{
|
|
if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17763)) return false;
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
var id = Guid.NewGuid().ToString("N");
|
|
var response = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
try
|
|
{
|
|
lock (gate)
|
|
{
|
|
if (!registered)
|
|
{
|
|
ToastNotificationManagerCompat.OnActivated += OnActivated;
|
|
registered = true;
|
|
}
|
|
|
|
pending[id] = response;
|
|
new ToastContentBuilder()
|
|
.AddArgument("source", Source)
|
|
.AddArgument("promptId", id)
|
|
.SetToastScenario(ToastScenario.Reminder)
|
|
.SetToastDuration(ToastDuration.Long)
|
|
.AddText("New microphone available")
|
|
.AddText($"Switch this meeting to {microphone.Name}?")
|
|
.AddButton(new ToastButton().SetContent("Yes")
|
|
.AddArgument("source", Source).AddArgument("promptId", id)
|
|
.AddArgument("response", "yes").SetBackgroundActivation())
|
|
.AddButton(new ToastButton().SetContent("No")
|
|
.AddArgument("source", Source).AddArgument("promptId", id)
|
|
.AddArgument("response", "no").SetBackgroundActivation())
|
|
.Show(toast =>
|
|
{
|
|
toast.Tag = id;
|
|
toast.Group = Source;
|
|
toast.ExpirationTime = DateTimeOffset.Now.AddMinutes(1);
|
|
});
|
|
}
|
|
|
|
logger.LogInformation("Offered newly available microphone {MicrophoneName}", microphone.Name);
|
|
return await response.Task.WaitAsync(cancellationToken);
|
|
}
|
|
finally
|
|
{
|
|
pending.TryRemove(id, out _);
|
|
try
|
|
{
|
|
ToastNotificationManagerCompat.History.Remove(id, Source);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
logger.LogDebug(exception, "Could not remove microphone notification {PromptId}", id);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnActivated(ToastNotificationActivatedEventArgsCompat args)
|
|
{
|
|
var arguments = NotificationActivationArguments.Parse(args.Argument);
|
|
if (arguments.GetValueOrDefault("source") != Source ||
|
|
!arguments.TryGetValue("promptId", out var id) ||
|
|
!pending.TryRemove(id, out var response)) return;
|
|
|
|
response.TrySetResult(arguments.GetValueOrDefault("response") == "yes");
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
lock (gate)
|
|
{
|
|
if (registered) ToastNotificationManagerCompat.OnActivated -= OnActivated;
|
|
registered = false;
|
|
foreach (var response in pending.Values) response.TrySetResult(false);
|
|
pending.Clear();
|
|
}
|
|
}
|
|
}
|
|
#endif
|