Fix notification activation and calendar bootstrap flow

This commit is contained in:
Burak Kaan Köse
2026-04-16 01:32:48 +02:00
parent 94675eee9a
commit e13aaadc78
15 changed files with 844 additions and 209 deletions
@@ -0,0 +1,40 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Windows.AppNotifications;
namespace Wino.Mail.WinUI.Activation;
internal sealed record BufferedAppNotificationActivation(string Argument, IReadOnlyDictionary<string, string>? UserInput);
internal sealed class AppNotificationActivationBuffer
{
private readonly ConcurrentQueue<BufferedAppNotificationActivation> _pendingActivations = new();
private readonly SemaphoreSlim _pendingSignal = new(0);
public void Enqueue(AppNotificationActivatedEventArgs args)
{
var copiedUserInput = args.UserInput == null
? null
: new Dictionary<string, string>(args.UserInput, StringComparer.Ordinal);
_pendingActivations.Enqueue(new BufferedAppNotificationActivation(args.Argument, copiedUserInput));
_pendingSignal.Release();
}
public bool TryDequeue(out BufferedAppNotificationActivation activation)
=> _pendingActivations.TryDequeue(out activation!);
public async Task<BufferedAppNotificationActivation?> WaitAsync(TimeSpan timeout, CancellationToken cancellationToken = default)
{
if (!_pendingActivations.IsEmpty && TryDequeue(out var queuedActivation))
return queuedActivation;
if (!await _pendingSignal.WaitAsync(timeout, cancellationToken))
return null;
return TryDequeue(out var activation) ? activation : null;
}
}
@@ -0,0 +1,207 @@
using System;
using System.IO;
using System.Linq;
using Microsoft.Windows.AppLifecycle;
using Windows.ApplicationModel;
using Windows.ApplicationModel.Activation;
using Windows.Storage;
using Wino.Core.Domain.Enums;
namespace Wino.Mail.WinUI.Activation;
internal enum PendingBootstrapActivationKind
{
Launch,
Protocol,
File
}
internal sealed class PendingBootstrapActivation
{
public PendingBootstrapActivationKind Kind { get; init; }
public WinoApplicationMode Mode { get; init; } = WinoApplicationMode.Mail;
public string? LaunchArguments { get; init; }
public string? TileId { get; init; }
public string? ProtocolUri { get; init; }
public string[] FilePaths { get; init; } = [];
public DateTimeOffset CreatedAtUtc { get; init; } = DateTimeOffset.UtcNow;
}
internal static class CalendarEntryBootstrapActivation
{
private const string PendingActivationKey = "PendingCalendarEntryBootstrapActivation";
private const string KindKey = "Kind";
private const string ModeKey = "Mode";
private const string LaunchArgumentsKey = "LaunchArguments";
private const string TileIdKey = "TileId";
private const string ProtocolUriKey = "ProtocolUri";
private const string FilePathsKey = "FilePaths";
private const string CreatedAtUtcKey = "CreatedAtUtc";
private static readonly TimeSpan PendingActivationLifetime = TimeSpan.FromMinutes(1);
public static bool ShouldBootstrapToMailHost(AppActivationArguments activationArgs)
=> TryCreatePendingActivation(activationArgs, out _);
public static bool QueuePendingActivation(AppActivationArguments activationArgs)
{
if (!TryCreatePendingActivation(activationArgs, out var pendingActivation))
return false;
ApplicationData.Current.LocalSettings.Values[PendingActivationKey] = CreateCompositeValue(pendingActivation!);
return true;
}
public static void ClearPendingActivation()
=> ApplicationData.Current.LocalSettings.Values.Remove(PendingActivationKey);
public static PendingBootstrapActivation? ConsumePendingActivation()
{
if (!ApplicationData.Current.LocalSettings.Values.TryGetValue(PendingActivationKey, out var pendingActivationValue) ||
pendingActivationValue is not ApplicationDataCompositeValue compositeValue)
{
return null;
}
ClearPendingActivation();
try
{
var pendingActivation = ParseCompositeValue(compositeValue);
if (pendingActivation == null)
return null;
if (DateTimeOffset.UtcNow - pendingActivation.CreatedAtUtc > PendingActivationLifetime)
return null;
return pendingActivation;
}
catch
{
return null;
}
}
private static ApplicationDataCompositeValue CreateCompositeValue(PendingBootstrapActivation pendingActivation)
{
var compositeValue = new ApplicationDataCompositeValue
{
[KindKey] = pendingActivation.Kind.ToString(),
[ModeKey] = pendingActivation.Mode.ToString(),
[LaunchArgumentsKey] = pendingActivation.LaunchArguments ?? string.Empty,
[TileIdKey] = pendingActivation.TileId ?? string.Empty,
[ProtocolUriKey] = pendingActivation.ProtocolUri ?? string.Empty,
[FilePathsKey] = string.Join("\n", pendingActivation.FilePaths),
[CreatedAtUtcKey] = pendingActivation.CreatedAtUtc.ToString("o")
};
return compositeValue;
}
private static PendingBootstrapActivation? ParseCompositeValue(ApplicationDataCompositeValue compositeValue)
{
if (!Enum.TryParse(compositeValue[KindKey]?.ToString(), ignoreCase: true, out PendingBootstrapActivationKind kind) ||
!Enum.TryParse(compositeValue[ModeKey]?.ToString(), ignoreCase: true, out WinoApplicationMode mode) ||
!DateTimeOffset.TryParse(compositeValue[CreatedAtUtcKey]?.ToString(), out var createdAtUtc))
{
return null;
}
return new PendingBootstrapActivation
{
Kind = kind,
Mode = mode,
LaunchArguments = GetOptionalCompositeString(compositeValue, LaunchArgumentsKey),
TileId = GetOptionalCompositeString(compositeValue, TileIdKey),
ProtocolUri = GetOptionalCompositeString(compositeValue, ProtocolUriKey),
FilePaths = GetOptionalCompositeString(compositeValue, FilePathsKey)?
.Split(['\n'], StringSplitOptions.RemoveEmptyEntries)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray() ?? [],
CreatedAtUtc = createdAtUtc
};
}
private static string? GetOptionalCompositeString(ApplicationDataCompositeValue compositeValue, string key)
{
if (!compositeValue.TryGetValue(key, out var value))
return null;
var stringValue = value?.ToString();
return string.IsNullOrWhiteSpace(stringValue) ? null : stringValue;
}
public static bool LaunchMailHost()
{
var mailAppUserModelId = AppEntryConstants.GetAppUserModelId(WinoApplicationMode.Mail);
var appEntries = Package.Current.GetAppListEntriesAsync().AsTask().GetAwaiter().GetResult();
var mailEntry = appEntries.FirstOrDefault(entry =>
string.Equals(entry.AppUserModelId, mailAppUserModelId, StringComparison.OrdinalIgnoreCase));
return mailEntry != null && mailEntry.LaunchAsync().AsTask().GetAwaiter().GetResult();
}
private static bool TryCreatePendingActivation(AppActivationArguments activationArgs, out PendingBootstrapActivation? pendingActivation)
{
pendingActivation = null;
if (activationArgs.Kind == ExtendedActivationKind.Launch &&
activationArgs.Data is ILaunchActivatedEventArgs launchArgs)
{
var resolvedMode = AppModeActivationResolver.Resolve(launchArgs.Arguments, launchArgs.TileId, Environment.CommandLine);
if (resolvedMode != WinoApplicationMode.Calendar)
return false;
pendingActivation = new PendingBootstrapActivation
{
Kind = PendingBootstrapActivationKind.Launch,
Mode = resolvedMode,
LaunchArguments = launchArgs.Arguments,
TileId = launchArgs.TileId
};
return true;
}
if (activationArgs.Kind == ExtendedActivationKind.Protocol &&
activationArgs.Data is IProtocolActivatedEventArgs protocolArgs &&
protocolArgs.Uri != null &&
(string.Equals(protocolArgs.Uri.Scheme, "webcal", StringComparison.OrdinalIgnoreCase) ||
string.Equals(protocolArgs.Uri.Scheme, "webcals", StringComparison.OrdinalIgnoreCase)))
{
pendingActivation = new PendingBootstrapActivation
{
Kind = PendingBootstrapActivationKind.Protocol,
Mode = WinoApplicationMode.Calendar,
ProtocolUri = protocolArgs.Uri.AbsoluteUri
};
return true;
}
if (activationArgs.Kind == ExtendedActivationKind.File &&
activationArgs.Data is IFileActivatedEventArgs fileArgs)
{
var filePaths = fileArgs.Files?
.OfType<IStorageItem>()
.Where(item => string.Equals(Path.GetExtension(item.Path), ".ics", StringComparison.OrdinalIgnoreCase))
.Select(item => item.Path)
.Where(path => !string.IsNullOrWhiteSpace(path))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
if (filePaths == null || filePaths.Length == 0)
return false;
pendingActivation = new PendingBootstrapActivation
{
Kind = PendingBootstrapActivationKind.File,
Mode = WinoApplicationMode.Calendar,
FilePaths = filePaths
};
return true;
}
return false;
}
}