Full trust Wino Server implementation. (#295)

* Separation of messages. Introducing Wino.Messages library.

* Wino.Server and Wino.Packaging projects. Enabling full trust for UWP and app service connection manager basics.

* Remove debug code.

* Enable generating assembly info to deal with unsupported os platform warnings.

* Fix server-client connection.

* UIMessage communication. Single instancing for server and re-connection mechanism on suspension.

* Removed IWinoSynchronizerFactory from UWP project.

* Removal of background task service from core.

* Delegating changes to UI and triggering new background synchronization.

* Fix build error.

* Moved core lib messages to Messaging project.

* Better client-server communication. Handling of requests in the server. New synchronizer factory in the server.

* WAM broker and MSAL token caching for OutlookAuthenticator. Handling account creation for Outlook.

* WinoServerResponse basics.

* Delegating protocol activation for Gmail authenticator.

* Adding margin to searchbox to match action bar width.

* Move libraries into lib folder.

* Storing base64 encoded mime on draft creation instead of MimeMessage object. Fixes serialization/deserialization issue with S.T.Json

* Scrollbar adjustments

* WınoExpander for thread expander layout ıssue.

* Handling synchronizer state changes.

* Double init on background activation.

* FIxing packaging issues and new Wino Mail launcher protocol for activation from full thrust process.

* Remove debug deserialization.

* Remove debug code.

* Making sure the server connection is established when the app is launched.

* Thrust -> Trust string replacement...

* Rename package to Wino Mail

* Enable translated values in the server.

* Fixed an issue where toast activation can't find the clicked mail after the folder is initialized.

* Revert debug code.

* Change server background sync to every 3 minute and Inbox only synchronization.

* Revert google auth changes.

* App preferences page.

* Changing tray icon visibility on preference change.

* Start the server with invisible tray icon if set to invisible.

* Reconnect button on the title bar.

* Handling of toast actions.

* Enable x86 build for server during packaging.

* Get rid of old background tasks and v180 migration.

* Terminate client when Exit clicked in server.

* Introducing SynchronizationSource to prevent notifying UI after server tick synchronization.

* Remove confirmAppClose restricted capability and unused debug code in manifest.

* Closing the reconnect info popup when reconnect is clicked.

* Custom RetryHandler for OutlookSynchronizer and separating client/server logs.

* Running server on Windows startup.

* Fix startup exe.

* Fix for expander list view item paddings.

* Force full sync on app launch instead of Inbox.

* Fix draft creation.

* Fix an issue with custom folder sync logic.

* Reporting back account sync progress from server.

* Fix sending drafts and missing notifications for imap.

* Changing imap folder sync requirements.

* Retain file  count is set to 3.

* Disabled swipe gestures temporarily due to native crash
 with SwipeControl

* Save all attachments implementation.

* Localization for save all attachments button.

* Fix logging dates for logs.

* Fixing ARM64 build.

* Add ARM64 build config to packaging project.

* Comment out OutOfProcPDB for ARM64.

* Hnadling GONE response for Outlook folder synchronization.
This commit is contained in:
Burak Kaan Köse
2024-08-05 00:36:26 +02:00
committed by GitHub
parent 4dc225184d
commit ff77b2b3dc
275 changed files with 4986 additions and 2381 deletions

View File

@@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using Windows.ApplicationModel.AppService;
using Wino.Core.Domain.Interfaces;
using Wino.Core.UWP.Services;
using Wino.Services;
@@ -9,19 +10,24 @@ namespace Wino.Core.UWP
{
public static void RegisterCoreUWPServices(this IServiceCollection services)
{
var serverConnectionManager = new WinoServerConnectionManager();
services.AddSingleton<IWinoServerConnectionManager>(serverConnectionManager);
services.AddSingleton<IWinoServerConnectionManager<AppServiceConnection>>(serverConnectionManager);
services.AddSingleton<IUnderlyingThemeService, UnderlyingThemeService>();
services.AddSingleton<INativeAppService, NativeAppService>();
services.AddSingleton<IStoreManagementService, StoreManagementService>();
services.AddSingleton<IBackgroundTaskService, BackgroundTaskService>();
services.AddTransient<IAppInitializerService, AppInitializerService>();
services.AddTransient<IConfigurationService, ConfigurationService>();
services.AddTransient<IFileService, FileService>();
services.AddTransient<IStoreRatingService, StoreRatingService>();
services.AddTransient<IKeyPressService, KeyPressService>();
services.AddTransient<IBackgroundSynchronizer, BackgroundSynchronizer>();
services.AddTransient<INotificationBuilder, NotificationBuilder>();
services.AddTransient<IClipboardService, ClipboardService>();
services.AddTransient<IStartupBehaviorService, StartupBehaviorService>();
}
}
}

View File

@@ -0,0 +1,25 @@
using Windows.ApplicationModel;
using Wino.Core.Domain.Enums;
namespace Wino.Core.UWP.Extensions
{
public static class StartupTaskStateExtensions
{
public static StartupBehaviorResult AsStartupBehaviorResult(this StartupTaskState state)
{
switch (state)
{
case StartupTaskState.Disabled:
case StartupTaskState.DisabledByPolicy:
return StartupBehaviorResult.Disabled;
case StartupTaskState.DisabledByUser:
return StartupBehaviorResult.DisabledByUser;
case StartupTaskState.Enabled:
case StartupTaskState.EnabledByPolicy:
return StartupBehaviorResult.Enabled;
default:
return StartupBehaviorResult.Fatal;
}
}
}
}

View File

@@ -1,50 +0,0 @@
using System;
using System.Threading.Tasks;
using Windows.Storage;
using Wino.Core.Domain.Interfaces;
namespace Wino.Core.UWP.Services
{
public class AppInitializerService : IAppInitializerService
{
private readonly IBackgroundTaskService _backgroundTaskService;
public AppInitializerService(IBackgroundTaskService backgroundTaskService)
{
_backgroundTaskService = backgroundTaskService;
}
public string GetPublisherSharedFolder() => ApplicationData.Current.GetPublisherCacheFolder("WinoShared").Path;
public string GetApplicationDataFolder() => ApplicationData.Current.LocalFolder.Path;
public Task MigrateAsync()
{
UnregisterAllBackgroundTasks();
return Task.CompletedTask;
}
#region 1.6.8 -> 1.6.9
private void UnregisterAllBackgroundTasks()
{
_backgroundTaskService.UnregisterAllBackgroundTask();
}
#endregion
#region 1.7.0
/// <summary>
/// We decided to use publisher cache folder as a database going forward.
/// This migration will move the file from application local folder and delete it.
/// Going forward database will be initialized from publisher cache folder.
/// </summary>
private async Task MoveExistingDatabaseToSharedCacheFolderAsync()
{
throw new NotImplementedException();
}
#endregion
}
}

View File

@@ -1,144 +0,0 @@
using System;
using System.Threading.Tasks;
using Serilog;
using Windows.Storage;
using Wino.Core;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Synchronizers;
namespace Wino.Services
{
public interface IBackgroundSynchronizer
{
Task RunBackgroundSynchronizationAsync(BackgroundSynchronizationReason reason);
void CreateLock();
void ReleaseLock();
bool IsBackgroundSynchronizationLocked();
}
/// <summary>
/// Service responsible for handling background synchronization on timer and session connected events.
/// </summary>
public class BackgroundSynchronizer : IBackgroundSynchronizer
{
private const string BackgroundSynchronizationLock = nameof(BackgroundSynchronizationLock);
private readonly IAccountService _accountService;
private readonly IFolderService _folderService;
private readonly IWinoSynchronizerFactory _winoSynchronizerFactory;
public BackgroundSynchronizer(IAccountService accountService,
IFolderService folderService,
IWinoSynchronizerFactory winoSynchronizerFactory)
{
_accountService = accountService;
_folderService = folderService;
_winoSynchronizerFactory = winoSynchronizerFactory;
}
public void CreateLock() => ApplicationData.Current.LocalSettings.Values[BackgroundSynchronizationLock] = true;
public void ReleaseLock() => ApplicationData.Current.LocalSettings.Values[BackgroundSynchronizationLock] = false;
public bool IsBackgroundSynchronizationLocked()
=> ApplicationData.Current.LocalSettings.Values.ContainsKey(BackgroundSynchronizationLock)
&& ApplicationData.Current.LocalSettings.Values[BackgroundSynchronizationLock] is bool boolValue && boolValue;
public async Task RunBackgroundSynchronizationAsync(BackgroundSynchronizationReason reason)
{
Log.Information($"{reason} background synchronization is kicked in.");
// This should never crash.
// We might be in-process or out-of-process.
//if (IsBackgroundSynchronizationLocked())
//{
// Log.Warning("Background synchronization is locked. Hence another background synchronization is canceled.");
// return;
//}
try
{
CreateLock();
var accounts = await _accountService.GetAccountsAsync();
foreach (var account in accounts)
{
// We can't sync broken account.
if (account.AttentionReason != AccountAttentionReason.None)
continue;
// TODO
// We can't synchronize without system folder setup is done.
//var isSystemFolderSetupDone = await _folderService.CheckSystemFolderSetupDoneAsync(account.Id);
//// No need to throw here. It's a background process.
//if (!isSystemFolderSetupDone)
// continue;
var synchronizer = _winoSynchronizerFactory.GetAccountSynchronizer(account.Id);
if (synchronizer.State != AccountSynchronizerState.Idle)
{
Log.Information("Skipping background synchronization for {Name} since current state is {State}", synchronizer.Account.Name, synchronizer.State);
return;
}
await HandleSynchronizationAsync(synchronizer, reason);
}
}
catch (Exception ex)
{
Log.Error($"[BackgroundSynchronization] Failed with message {ex.Message}");
}
finally
{
ReleaseLock();
}
}
private async Task HandleSynchronizationAsync(IBaseSynchronizer synchronizer, BackgroundSynchronizationReason reason)
{
if (synchronizer.State != AccountSynchronizerState.Idle) return;
var account = synchronizer.Account;
try
{
// SessionConnected will do Full synchronization for logon, Timer task will do Inbox only.
var syncType = reason == BackgroundSynchronizationReason.SessionConnected ? SynchronizationType.Full : SynchronizationType.Inbox;
var options = new SynchronizationOptions()
{
AccountId = account.Id,
Type = syncType,
};
await synchronizer.SynchronizeAsync(options);
}
catch (AuthenticationAttentionException authenticationAttentionException)
{
Log.Error(authenticationAttentionException, $"[BackgroundSync] Invalid credentials for account {account.Address}");
account.AttentionReason = AccountAttentionReason.InvalidCredentials;
await _accountService.UpdateAccountAsync(account);
}
catch (SystemFolderConfigurationMissingException configMissingException)
{
Log.Error(configMissingException, $"[BackgroundSync] Missing system folder configuration for account {account.Address}");
account.AttentionReason = AccountAttentionReason.MissingSystemFolderConfiguration;
await _accountService.UpdateAccountAsync(account);
}
catch (Exception ex)
{
Log.Error(ex, "[BackgroundSync] Synchronization failed.");
}
}
}
}

View File

@@ -1,92 +1,60 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Serilog;
using Windows.ApplicationModel.Background;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Exceptions;
namespace Wino.Core.UWP.Services
{
public class BackgroundTaskService : IBackgroundTaskService
{
private const string IsBackgroundExecutionDeniedMessageKey = nameof(IsBackgroundExecutionDeniedMessageKey);
private const string Is180BackgroundTasksRegisteredKey = nameof(Is180BackgroundTasksRegisteredKey);
public const string BackgroundSynchronizationTimerTaskNameEx = nameof(BackgroundSynchronizationTimerTaskNameEx);
public const string ToastActivationTaskEx = nameof(ToastActivationTaskEx);
private const string SessionConnectedTaskEntryPoint = "Wino.BackgroundTasks.SessionConnectedTask";
private const string SessionConnectedTaskName = "SessionConnectedTask";
private readonly IConfigurationService _configurationService;
private readonly List<string> registeredBackgroundTaskNames = new List<string>();
public BackgroundTaskService(IConfigurationService configurationService)
{
_configurationService = configurationService;
LoadRegisteredTasks();
}
// Calling WinRT all the time for registered tasks might be slow. Cache them on ctor.
private void LoadRegisteredTasks()
{
foreach (var task in BackgroundTaskRegistration.AllTasks)
{
registeredBackgroundTaskNames.Add(task.Value.Name);
}
Log.Information($"Found {registeredBackgroundTaskNames.Count} registered background tasks. [{string.Join(',', registeredBackgroundTaskNames)}]");
}
public async Task HandleBackgroundTaskRegistrations()
{
bool is180BackgroundTaskRegistered = _configurationService.Get<bool>(Is180BackgroundTasksRegisteredKey);
// Don't re-register tasks.
if (is180BackgroundTaskRegistered) return;
var response = await BackgroundExecutionManager.RequestAccessAsync();
if (response == BackgroundAccessStatus.DeniedBySystemPolicy ||
response == BackgroundAccessStatus.DeniedByUser)
if (response != BackgroundAccessStatus.DeniedBySystemPolicy ||
response != BackgroundAccessStatus.DeniedByUser)
{
// Only notify users about disabled background execution once.
// Unregister all tasks and register new ones.
bool isNotifiedBefore = _configurationService.Get(IsBackgroundExecutionDeniedMessageKey, false);
if (!isNotifiedBefore)
{
_configurationService.Set(IsBackgroundExecutionDeniedMessageKey, true);
throw new BackgroundTaskExecutionRequestDeniedException();
}
}
else
{
UnregisterAllBackgroundTask();
RegisterSessionConnectedTask();
RegisterTimerSynchronizationTask();
RegisterToastNotificationHandlerBackgroundTask();
_configurationService.Set(Is180BackgroundTasksRegisteredKey, true);
}
}
private bool IsBackgroundTaskRegistered(string taskName)
=> registeredBackgroundTaskNames.Contains(taskName);
public void UnregisterAllBackgroundTask()
{
foreach (var task in BackgroundTaskRegistration.AllTasks)
{
task.Value.Unregister(true);
}
}
private void LogBackgroundTaskRegistration(string taskName)
{
Log.Information($"Registered new background task -> {taskName}");
registeredBackgroundTaskNames.Add($"{taskName}");
Log.Information("Unregistered all background tasks.");
}
private BackgroundTaskRegistration RegisterSessionConnectedTask()
{
if (IsBackgroundTaskRegistered(SessionConnectedTaskName)) return null;
var builder = new BackgroundTaskBuilder
{
Name = SessionConnectedTaskName,
@@ -95,41 +63,6 @@ namespace Wino.Core.UWP.Services
builder.SetTrigger(new SystemTrigger(SystemTriggerType.SessionConnected, false));
LogBackgroundTaskRegistration(SessionConnectedTaskName);
return builder.Register();
}
private BackgroundTaskRegistration RegisterToastNotificationHandlerBackgroundTask()
{
if (IsBackgroundTaskRegistered(ToastActivationTaskEx)) return null;
var builder = new BackgroundTaskBuilder
{
Name = ToastActivationTaskEx
};
builder.SetTrigger(new ToastNotificationActionTrigger());
LogBackgroundTaskRegistration(ToastActivationTaskEx);
return builder.Register();
}
private BackgroundTaskRegistration RegisterTimerSynchronizationTask()
{
if (IsBackgroundTaskRegistered(BackgroundSynchronizationTimerTaskNameEx)) return null;
var builder = new BackgroundTaskBuilder
{
Name = BackgroundSynchronizationTimerTaskNameEx
};
builder.SetTrigger(new TimeTrigger(15, false));
builder.AddCondition(new SystemCondition(SystemConditionType.InternetAvailable));
LogBackgroundTaskRegistration(BackgroundSynchronizationTimerTaskNameEx);
return builder.Register();
}
}

View File

@@ -9,10 +9,17 @@ using Windows.Storage;
using Windows.Storage.Streams;
using Windows.System;
using Windows.UI.Shell;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Authorization;
using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain;
#if WINDOWS_UWP
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
#endif
namespace Wino.Services
{
@@ -20,8 +27,18 @@ namespace Wino.Services
{
private string _mimeMessagesFolder;
private string _editorBundlePath;
private TaskCompletionSource<Uri> authorizationCompletedTaskSource;
public string GetWebAuthenticationBrokerUri() => WebAuthenticationBroker.GetCurrentApplicationCallbackUri().AbsoluteUri;
public Func<IntPtr> GetCoreWindowHwnd { get; set; }
public string GetWebAuthenticationBrokerUri()
{
#if WINDOWS_UWP
return WebAuthenticationBroker.GetCurrentApplicationCallbackUri().AbsoluteUri;
#endif
return string.Empty;
}
public async Task<string> GetMimeMessageStoragePath()
{
@@ -91,7 +108,16 @@ namespace Wino.Services
return _editorBundlePath;
}
public bool IsAppRunning() => (Window.Current?.Content as Frame)?.Content != null;
[Obsolete("This should be removed. There should be no functionality.")]
public bool IsAppRunning()
{
#if WINDOWS_UWP
return (Window.Current?.Content as Frame)?.Content != null;
#endif
return true;
}
public async Task LaunchFileAsync(string filePath)
{
@@ -100,7 +126,7 @@ namespace Wino.Services
await Launcher.LaunchFileAsync(file);
}
public Task LaunchUriAsync(Uri uri) => Xamarin.Essentials.Launcher.OpenAsync(uri);
public Task LaunchUriAsync(Uri uri) => Launcher.LaunchUriAsync(uri).AsTask();
public string GetFullAppVersion()
{
@@ -127,5 +153,28 @@ namespace Wino.Services
await taskbarManager.RequestPinCurrentAppAsync();
}
public async Task<Uri> GetAuthorizationResponseUriAsync(IAuthenticator authenticator, string authorizationUri)
{
if (authorizationCompletedTaskSource != null)
{
authorizationCompletedTaskSource.TrySetException(new AuthenticationException(Translator.Exception_AuthenticationCanceled));
authorizationCompletedTaskSource = null;
}
authorizationCompletedTaskSource = new TaskCompletionSource<Uri>();
await LaunchUriAsync(new Uri(authorizationUri));
return await authorizationCompletedTaskSource.Task;
}
public void ContinueAuthorization(Uri authorizationResponseUri)
{
if (authorizationCompletedTaskSource != null)
{
authorizationCompletedTaskSource.TrySetResult(authorizationResponseUri);
}
}
}
}

View File

@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Toolkit.Uwp.Notifications;
using CommunityToolkit.WinUI.Notifications;
using Windows.Data.Xml.Dom;
using Windows.UI.Notifications;
using Wino.Core.Domain;
@@ -70,8 +70,8 @@ namespace Wino.Core.UWP.Services
foreach (var mailItem in validItems)
{
if (mailItem.IsRead)
continue;
//if (mailItem.IsRead)
// continue;
var builder = new ToastContentBuilder();
builder.SetToastScenario(ToastScenario.Default);
@@ -104,11 +104,11 @@ namespace Wino.Core.UWP.Services
builder.AddText(mailItem.Subject);
builder.AddText(mailItem.PreviewText);
builder.AddArgument(Constants.ToastMailItemIdKey, mailItem.UniqueId.ToString());
builder.AddArgument(Constants.ToastMailUniqueIdKey, mailItem.UniqueId.ToString());
builder.AddArgument(Constants.ToastActionKey, MailOperation.Navigate);
builder.AddButton(GetMarkedAsRead(mailItem.Id, mailItem.AssignedFolder.RemoteFolderId));
builder.AddButton(GetDeleteButton(mailItem.Id, mailItem.AssignedFolder.RemoteFolderId));
builder.AddButton(GetMarkedAsRead(mailItem.UniqueId));
builder.AddButton(GetDeleteButton(mailItem.UniqueId));
builder.AddButton(GetDismissButton());
builder.Show();
@@ -123,21 +123,19 @@ namespace Wino.Core.UWP.Services
.SetDismissActivation()
.SetImageUri(new Uri("ms-appx:///Assets/NotificationIcons/dismiss.png"));
private ToastButton GetDeleteButton(string mailCopyId, string remoteFolderId)
private ToastButton GetDeleteButton(Guid mailUniqueId)
=> new ToastButton()
.SetContent(Translator.MailOperation_Delete)
.SetImageUri(new Uri("ms-appx:///Assets/NotificationIcons/delete.png"))
.AddArgument(Constants.ToastMailItemIdKey, mailCopyId)
.AddArgument(Constants.ToastMailItemRemoteFolderIdKey, remoteFolderId)
.AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString())
.AddArgument(Constants.ToastActionKey, MailOperation.SoftDelete)
.SetBackgroundActivation();
private ToastButton GetMarkedAsRead(string mailCopyId, string remoteFolderId)
private ToastButton GetMarkedAsRead(Guid mailUniqueId)
=> new ToastButton()
.SetContent(Translator.MailOperation_MarkAsRead)
.SetImageUri(new System.Uri("ms-appx:///Assets/NotificationIcons/markread.png"))
.AddArgument(Constants.ToastMailItemIdKey, mailCopyId)
.AddArgument(Constants.ToastMailItemRemoteFolderIdKey, remoteFolderId)
.AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString())
.AddArgument(Constants.ToastActionKey, MailOperation.MarkAsRead)
.SetBackgroundActivation();

View File

@@ -0,0 +1,211 @@
using System;
using System.ComponentModel;
using System.Diagnostics;
using CommunityToolkit.Mvvm.ComponentModel;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Reader;
using Wino.Core.Services;
namespace Wino.Core.UWP.Services
{
public class PreferencesService : ObservableObject, IPreferencesService
{
private readonly IConfigurationService _configurationService;
public event EventHandler<string> PreferenceChanged;
public PreferencesService(IConfigurationService configurationService)
{
_configurationService = configurationService;
}
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
PreferenceChanged?.Invoke(this, e.PropertyName);
}
private void SaveProperty(string propertyName, object value) => _configurationService.Set(propertyName, value);
private void SetPropertyAndSave(string propertyName, object value)
{
_configurationService.Set(propertyName, value);
OnPropertyChanged(propertyName);
Debug.WriteLine($"PreferencesService -> {propertyName}:{value?.ToString()}");
}
public MailRenderingOptions GetRenderingOptions()
=> new MailRenderingOptions() { LoadImages = RenderImages, LoadStyles = RenderStyles };
public MailListDisplayMode MailItemDisplayMode
{
get => _configurationService.Get(nameof(MailItemDisplayMode), MailListDisplayMode.Spacious);
set => SetPropertyAndSave(nameof(MailItemDisplayMode), value);
}
public bool IsSemanticZoomEnabled
{
get => _configurationService.Get(nameof(IsSemanticZoomEnabled), true);
set => SetPropertyAndSave(nameof(IsSemanticZoomEnabled), value);
}
public bool IsHardDeleteProtectionEnabled
{
get => _configurationService.Get(nameof(IsHardDeleteProtectionEnabled), true);
set => SetPropertyAndSave(nameof(IsHardDeleteProtectionEnabled), value);
}
public bool IsThreadingEnabled
{
get => _configurationService.Get(nameof(IsThreadingEnabled), true);
set => SetPropertyAndSave(nameof(IsThreadingEnabled), value);
}
public bool IsShowSenderPicturesEnabled
{
get => _configurationService.Get(nameof(IsShowSenderPicturesEnabled), true);
set => SetPropertyAndSave(nameof(IsShowSenderPicturesEnabled), value);
}
public bool IsShowPreviewEnabled
{
get => _configurationService.Get(nameof(IsShowPreviewEnabled), true);
set => SetPropertyAndSave(nameof(IsShowPreviewEnabled), value);
}
public bool RenderStyles
{
get => _configurationService.Get(nameof(RenderStyles), true);
set => SetPropertyAndSave(nameof(RenderStyles), value);
}
public bool RenderImages
{
get => _configurationService.Get(nameof(RenderImages), true);
set => SetPropertyAndSave(nameof(RenderImages), value);
}
public bool Prefer24HourTimeFormat
{
get => _configurationService.Get(nameof(Prefer24HourTimeFormat), false);
set => SetPropertyAndSave(nameof(Prefer24HourTimeFormat), value);
}
public MailMarkAsOption MarkAsPreference
{
get => _configurationService.Get(nameof(MarkAsPreference), MailMarkAsOption.WhenSelected);
set => SetPropertyAndSave(nameof(MarkAsPreference), value);
}
public int MarkAsDelay
{
get => _configurationService.Get(nameof(MarkAsDelay), 5);
set => SetPropertyAndSave(nameof(MarkAsDelay), value);
}
public MailOperation RightSwipeOperation
{
get => _configurationService.Get(nameof(RightSwipeOperation), MailOperation.MarkAsRead);
set => SetPropertyAndSave(nameof(RightSwipeOperation), value);
}
public MailOperation LeftSwipeOperation
{
get => _configurationService.Get(nameof(LeftSwipeOperation), MailOperation.SoftDelete);
set => SetPropertyAndSave(nameof(LeftSwipeOperation), value);
}
public bool IsHoverActionsEnabled
{
get => _configurationService.Get(nameof(IsHoverActionsEnabled), true);
set => SetPropertyAndSave(nameof(IsHoverActionsEnabled), value);
}
public MailOperation LeftHoverAction
{
get => _configurationService.Get(nameof(LeftHoverAction), MailOperation.Archive);
set => SetPropertyAndSave(nameof(LeftHoverAction), value);
}
public MailOperation CenterHoverAction
{
get => _configurationService.Get(nameof(CenterHoverAction), MailOperation.SoftDelete);
set => SetPropertyAndSave(nameof(CenterHoverAction), value);
}
public MailOperation RightHoverAction
{
get => _configurationService.Get(nameof(RightHoverAction), MailOperation.SetFlag);
set => SetPropertyAndSave(nameof(RightHoverAction), value);
}
public bool IsLoggingEnabled
{
get => _configurationService.Get(nameof(IsLoggingEnabled), true);
set => SetPropertyAndSave(nameof(IsLoggingEnabled), value);
}
public bool IsMailkitProtocolLoggerEnabled
{
get => _configurationService.Get(nameof(IsMailkitProtocolLoggerEnabled), false);
set => SetPropertyAndSave(nameof(IsMailkitProtocolLoggerEnabled), value);
}
public Guid? StartupEntityId
{
get => _configurationService.Get<Guid?>(nameof(StartupEntityId), null);
set => SaveProperty(propertyName: nameof(StartupEntityId), value);
}
public AppLanguage CurrentLanguage
{
get => _configurationService.Get(nameof(CurrentLanguage), TranslationService.DefaultAppLanguage);
set => SaveProperty(propertyName: nameof(CurrentLanguage), value);
}
public string ReaderFont
{
get => _configurationService.Get(nameof(ReaderFont), "Calibri");
set => SaveProperty(propertyName: nameof(ReaderFont), value);
}
public int ReaderFontSize
{
get => _configurationService.Get(nameof(ReaderFontSize), 14);
set => SaveProperty(propertyName: nameof(ReaderFontSize), value);
}
public string ComposerFont
{
get => _configurationService.Get(nameof(ComposerFont), "Calibri");
set => SaveProperty(propertyName: nameof(ComposerFont), value);
}
public int ComposerFontSize
{
get => _configurationService.Get(nameof(ComposerFontSize), 14);
set => SaveProperty(propertyName: nameof(ComposerFontSize), value);
}
public bool IsNavigationPaneOpened
{
get => _configurationService.Get(nameof(IsNavigationPaneOpened), true);
set => SaveProperty(propertyName: nameof(IsNavigationPaneOpened), value);
}
public bool AutoSelectNextItem
{
get => _configurationService.Get(nameof(AutoSelectNextItem), true);
set => SaveProperty(propertyName: nameof(AutoSelectNextItem), value);
}
public ServerBackgroundMode ServerTerminationBehavior
{
get => _configurationService.Get(nameof(ServerTerminationBehavior), ServerBackgroundMode.MinimizedTray);
set => SaveProperty(propertyName: nameof(ServerTerminationBehavior), value);
}
}
}

View File

@@ -0,0 +1,37 @@
using System;
using System.Threading.Tasks;
using Windows.ApplicationModel;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.UWP.Extensions;
namespace Wino.Core.UWP.Services
{
public class StartupBehaviorService : IStartupBehaviorService
{
private const string WinoServerTaskId = "WinoServer";
public async Task<StartupBehaviorResult> ToggleStartupBehavior(bool isEnabled)
{
var task = await StartupTask.GetAsync(WinoServerTaskId);
if (isEnabled)
{
await task.RequestEnableAsync();
}
else
{
task.Disable();
}
return await GetCurrentStartupBehaviorAsync();
}
public async Task<StartupBehaviorResult> GetCurrentStartupBehaviorAsync()
{
var task = await StartupTask.GetAsync(WinoServerTaskId);
return task.State.AsStartupBehaviorResult();
}
}
}

View File

@@ -0,0 +1,124 @@
using System;
using System.ComponentModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Wino.Core.Domain.Interfaces;
using Wino.Messaging.Client.Shell;
namespace Wino.Services
{
public class StatePersistenceService : ObservableObject, IStatePersistanceService
{
public event EventHandler<string> StatePropertyChanged;
private const string OpenPaneLengthKey = nameof(OpenPaneLengthKey);
private const string MailListPaneLengthKey = nameof(MailListPaneLengthKey);
private readonly IConfigurationService _configurationService;
public StatePersistenceService(IConfigurationService configurationService)
{
_configurationService = configurationService;
openPaneLength = _configurationService.Get(OpenPaneLengthKey, 320d);
_mailListPaneLength = _configurationService.Get(MailListPaneLengthKey, 420d);
PropertyChanged += ServicePropertyChanged;
}
private void ServicePropertyChanged(object sender, PropertyChangedEventArgs e) => StatePropertyChanged?.Invoke(this, e.PropertyName);
public bool IsBackButtonVisible => IsReadingMail && IsReaderNarrowed;
private bool isReadingMail;
public bool IsReadingMail
{
get => isReadingMail;
set
{
if (SetProperty(ref isReadingMail, value))
{
OnPropertyChanged(nameof(IsBackButtonVisible));
WeakReferenceMessenger.Default.Send(new ShellStateUpdated());
}
}
}
private bool shouldShiftMailRenderingDesign;
public bool ShouldShiftMailRenderingDesign
{
get { return shouldShiftMailRenderingDesign; }
set { shouldShiftMailRenderingDesign = value; }
}
private bool isReaderNarrowed;
public bool IsReaderNarrowed
{
get => isReaderNarrowed;
set
{
if (SetProperty(ref isReaderNarrowed, value))
{
OnPropertyChanged(nameof(IsBackButtonVisible));
WeakReferenceMessenger.Default.Send(new ShellStateUpdated());
}
}
}
private string coreWindowTitle;
public string CoreWindowTitle
{
get => coreWindowTitle;
set
{
if (SetProperty(ref coreWindowTitle, value))
{
UpdateAppCoreWindowTitle();
}
}
}
#region Settings
private double openPaneLength;
public double OpenPaneLength
{
get => openPaneLength;
set
{
if (SetProperty(ref openPaneLength, value))
{
_configurationService.Set(OpenPaneLengthKey, value);
}
}
}
private double _mailListPaneLength;
public double MailListPaneLength
{
get => _mailListPaneLength;
set
{
if (SetProperty(ref _mailListPaneLength, value))
{
_configurationService.Set(MailListPaneLengthKey, value);
}
}
}
#endregion
private void UpdateAppCoreWindowTitle()
{
var appView = Windows.UI.ViewManagement.ApplicationView.GetForCurrentView();
if (appView != null)
appView.Title = CoreWindowTitle;
}
}
}

View File

@@ -20,10 +20,10 @@ using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Personalization;
using Wino.Core.Messages.Shell;
using Wino.Core.UWP.Extensions;
using Wino.Core.UWP.Models.Personalization;
using Wino.Core.UWP.Services;
using Wino.Messaging.Client.Shell;
namespace Wino.Services
{
@@ -167,6 +167,7 @@ namespace Wino.Services
await ApplyCustomThemeAsync(true);
// Registering to color changes, thus we notice when user changes theme system wide
uiSettings.ColorValuesChanged -= UISettingsColorChanged;
uiSettings.ColorValuesChanged += UISettingsColorChanged;
}

View File

@@ -1,5 +1,5 @@
using Windows.UI.ViewManagement;
using Windows.UI.Xaml;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
namespace Wino.Core.UWP.Services
@@ -21,12 +21,12 @@ namespace Wino.Core.UWP.Services
public bool IsUnderlyingThemeDark()
{
var currentTheme = _configurationService.Get(SelectedAppThemeKey, ElementTheme.Default);
var currentTheme = _configurationService.Get(SelectedAppThemeKey, ApplicationElementTheme.Default);
if (currentTheme == ElementTheme.Default)
if (currentTheme == ApplicationElementTheme.Default)
return uiSettings.GetColorValue(UIColorType.Background).ToString() == "#FF000000";
else
return currentTheme == ElementTheme.Dark;
return currentTheme == ApplicationElementTheme.Dark;
}
}
}

View File

@@ -0,0 +1,316 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging;
using Nito.AsyncEx;
using Serilog;
using Windows.ApplicationModel;
using Windows.ApplicationModel.AppService;
using Windows.Foundation.Collections;
using Windows.Foundation.Metadata;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Requests;
using Wino.Core.Domain.Models.Server;
using Wino.Core.Integration.Json;
using Wino.Messaging;
using Wino.Messaging.Client.Connection;
using Wino.Messaging.Enums;
using Wino.Messaging.UI;
namespace Wino.Core.UWP.Services
{
public class WinoServerConnectionManager :
IWinoServerConnectionManager<AppServiceConnection>,
IRecipient<WinoServerConnectionEstrablished>
{
private const int ServerConnectionTimeoutMs = 5000;
public event EventHandler<WinoServerConnectionStatus> StatusChanged;
private TaskCompletionSource<bool> _connectionTaskCompletionSource;
private ILogger Logger => Logger.ForContext<WinoServerConnectionManager>();
private WinoServerConnectionStatus status;
public WinoServerConnectionStatus Status
{
get { return status; }
private set
{
status = value;
StatusChanged?.Invoke(this, value);
}
}
private AppServiceConnection _connection;
public AppServiceConnection Connection
{
get { return _connection; }
set
{
if (_connection != null)
{
_connection.RequestReceived -= ServerMessageReceived;
_connection.ServiceClosed -= ServerDisconnected;
}
_connection = value;
if (value == null)
{
Status = WinoServerConnectionStatus.Disconnected;
}
else
{
value.RequestReceived += ServerMessageReceived;
value.ServiceClosed += ServerDisconnected;
Status = WinoServerConnectionStatus.Connected;
}
}
}
private readonly JsonSerializerOptions _jsonSerializerOptions = new()
{
TypeInfoResolver = new ServerRequestTypeInfoResolver()
};
public WinoServerConnectionManager()
{
WeakReferenceMessenger.Default.Register(this);
}
public async Task<bool> ConnectAsync()
{
if (Status == WinoServerConnectionStatus.Connected) return true;
if (ApiInformation.IsApiContractPresent("Windows.ApplicationModel.FullTrustAppContract", 1, 0))
{
try
{
_connectionTaskCompletionSource ??= new TaskCompletionSource<bool>();
var connectionCancellationToken = new CancellationTokenSource(TimeSpan.FromMilliseconds(ServerConnectionTimeoutMs));
Status = WinoServerConnectionStatus.Connecting;
await FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync();
// Connection establishment handler is in App.xaml.cs OnBackgroundActivated.
// Once the connection is established, the handler will set the Connection property
// and WinoServerConnectionEstrablished will be fired by the messenger.
await _connectionTaskCompletionSource.Task.WaitAsync(connectionCancellationToken.Token);
}
catch (Exception)
{
Status = WinoServerConnectionStatus.Failed;
return false;
}
return true;
}
return false;
}
public async Task<bool> DisconnectAsync()
{
if (Connection == null || Status == WinoServerConnectionStatus.Disconnected) return true;
// TODO: Send disconnect message to the fulltrust process.
return true;
}
public async Task InitializeAsync()
{
var isConnectionSuccessfull = await ConnectAsync();
// TODO: Log connection status
}
private void ServerMessageReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args)
{
if (args.Request.Message.TryGetValue(MessageConstants.MessageTypeKey, out object messageTypeObject) && messageTypeObject is int messageTypeInt)
{
var messageType = (MessageType)messageTypeInt;
if (args.Request.Message.TryGetValue(MessageConstants.MessageDataKey, out object messageDataObject) && messageDataObject is string messageJson)
{
switch (messageType)
{
case MessageType.UIMessage:
if (!args.Request.Message.TryGetValue(MessageConstants.MessageDataTypeKey, out object dataTypeObject) || dataTypeObject is not string dataTypeName)
throw new ArgumentException("Message data type is missing.");
HandleUIMessage(messageJson, dataTypeName);
break;
default:
break;
}
}
}
}
/// <summary>
/// Unpacks IServerMessage objects and delegate it to Messenger for UI to process.
/// </summary>
/// <param name="messageJson">Message data in json format.</param>
private void HandleUIMessage(string messageJson, string typeName)
{
switch (typeName)
{
case nameof(MailAddedMessage):
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<MailAddedMessage>(messageJson));
break;
case nameof(MailDownloadedMessage):
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<MailDownloadedMessage>(messageJson));
break;
case nameof(MailRemovedMessage):
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<MailRemovedMessage>(messageJson));
break;
case nameof(MailUpdatedMessage):
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<MailUpdatedMessage>(messageJson));
break;
case nameof(AccountCreatedMessage):
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<AccountCreatedMessage>(messageJson));
break;
case nameof(AccountRemovedMessage):
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<AccountRemovedMessage>(messageJson));
break;
case nameof(AccountUpdatedMessage):
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<AccountUpdatedMessage>(messageJson));
break;
case nameof(DraftCreated):
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<DraftCreated>(messageJson));
break;
case nameof(DraftFailed):
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<DraftFailed>(messageJson));
break;
case nameof(DraftMapped):
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<DraftMapped>(messageJson));
break;
case nameof(FolderRenamed):
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<FolderRenamed>(messageJson));
break;
case nameof(FolderSynchronizationEnabled):
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<FolderSynchronizationEnabled>(messageJson));
break;
case nameof(MergedInboxRenamed):
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<MergedInboxRenamed>(messageJson));
break;
case nameof(AccountSynchronizationCompleted):
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<AccountSynchronizationCompleted>(messageJson));
break;
case nameof(RefreshUnreadCountsMessage):
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<RefreshUnreadCountsMessage>(messageJson));
break;
case nameof(AccountSynchronizerStateChanged):
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<AccountSynchronizerStateChanged>(messageJson));
break;
case nameof(AccountSynchronizationProgressUpdatedMessage):
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<AccountSynchronizationProgressUpdatedMessage>(messageJson));
break;
default:
throw new Exception("Invalid data type name passed to client.");
}
}
private void ServerDisconnected(AppServiceConnection sender, AppServiceClosedEventArgs args)
{
// TODO: Handle server disconnection.
}
public async Task QueueRequestAsync(IRequestBase request, Guid accountId)
{
var queuePackage = new ServerRequestPackage(accountId, request);
var queueResponse = await GetResponseInternalAsync<bool, ServerRequestPackage>(queuePackage, new Dictionary<string, object>()
{
{ MessageConstants.MessageDataRequestAccountIdKey, accountId }
});
queueResponse.ThrowIfFailed();
}
public Task<WinoServerResponse<TResponse>> GetResponseAsync<TResponse, TRequestType>(TRequestType message) where TRequestType : IClientMessage
=> GetResponseInternalAsync<TResponse, TRequestType>(message);
private async Task<WinoServerResponse<TResponse>> GetResponseInternalAsync<TResponse, TRequestType>(TRequestType message, Dictionary<string, object> parameters = null)
{
if (Connection == null)
return WinoServerResponse<TResponse>.CreateErrorResponse("Server connection is not established.");
string serializedMessage = string.Empty;
try
{
serializedMessage = JsonSerializer.Serialize(message, _jsonSerializerOptions);
}
catch (Exception serializationException)
{
Logger.Error(serializationException, $"Failed to serialize client message for sending.");
return WinoServerResponse<TResponse>.CreateErrorResponse($"Failed to serialize message.\n{serializationException.Message}");
}
AppServiceResponse response = null;
try
{
var valueSet = new ValueSet
{
{ MessageConstants.MessageTypeKey, (int)MessageType.ServerMessage },
{ MessageConstants.MessageDataKey, serializedMessage },
{ MessageConstants.MessageDataTypeKey, message.GetType().Name }
};
// Add additional parameters into ValueSet
if (parameters != null)
{
foreach (var item in parameters)
{
valueSet.Add(item.Key, item.Value);
}
}
response = await Connection.SendMessageAsync(valueSet);
}
catch (Exception serverSendException)
{
Logger.Error(serverSendException, $"Failed to send message to server.");
return WinoServerResponse<TResponse>.CreateErrorResponse($"Failed to send message to server.\n{serverSendException.Message}");
}
// It should be always Success.
if (response.Status != AppServiceResponseStatus.Success)
return WinoServerResponse<TResponse>.CreateErrorResponse($"Wino Server responded with '{response.Status}' status to message delivery.");
// All responses must contain a message data.
if (!(response.Message.TryGetValue(MessageConstants.MessageDataKey, out object messageDataObject) && messageDataObject is string messageJson))
return WinoServerResponse<TResponse>.CreateErrorResponse("Server response did not contain message data.");
// Try deserialize the message data.
try
{
return JsonSerializer.Deserialize<WinoServerResponse<TResponse>>(messageJson);
}
catch (Exception jsonDeserializationError)
{
Logger.Error(jsonDeserializationError, $"Failed to deserialize server response message data.");
return WinoServerResponse<TResponse>.CreateErrorResponse($"Failed to deserialize Wino server response message data.\n{jsonDeserializationError.Message}");
}
}
public void Receive(WinoServerConnectionEstrablished message)
{
if (_connectionTaskCompletionSource != null)
{
_connectionTaskCompletionSource.TrySetResult(true);
}
}
}
}

View File

@@ -17,25 +17,6 @@
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
<PlatformTarget>x86</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
@@ -118,17 +99,21 @@
</PropertyGroup>
<PropertyGroup>
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
<LangVersion>12.0</LangVersion>
</PropertyGroup>
<ItemGroup>
<Compile Include="CoreUWPContainerSetup.cs" />
<Compile Include="Dispatcher.cs" />
<Compile Include="Extensions\ElementThemeExtensions.cs" />
<Compile Include="Extensions\StartupTaskStateExtensions.cs" />
<Compile Include="Models\Personalization\CustomAppTheme.cs" />
<Compile Include="Models\Personalization\PreDefinedAppTheme.cs" />
<Compile Include="Models\Personalization\SystemAppTheme.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Services\AppInitializerService.cs" />
<Compile Include="Services\BackgroundSynchronizer.cs" />
<Compile Include="Services\PreferencesService.cs" />
<Compile Include="Services\StartupBehaviorService.cs" />
<Compile Include="Services\StatePersistenceService.cs" />
<Compile Include="Services\WinoServerConnectionManager.cs" />
<Compile Include="Services\BackgroundTaskService.cs" />
<Compile Include="Services\ClipboardService.cs" />
<Compile Include="Services\ConfigurationService.cs" />
@@ -143,9 +128,9 @@
<EmbeddedResource Include="Properties\Wino.Core.UWP.rd.xml" />
</ItemGroup>
<ItemGroup>
<!--<PackageReference Include="CommunityToolkit.Uwp.Helpers">
<Version>8.0.230907</Version>
</PackageReference>-->
<PackageReference Include="CommunityToolkit.WinUI.Notifications">
<Version>7.1.2</Version>
</PackageReference>
<PackageReference Include="Microsoft.AppCenter.Analytics">
<Version>5.0.4</Version>
</PackageReference>
@@ -155,9 +140,6 @@
<PackageReference Include="Microsoft.Toolkit.Uwp">
<Version>7.1.3</Version>
</PackageReference>
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications">
<Version>7.1.3</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj">
@@ -168,8 +150,16 @@
<Project>{e6b1632a-8901-41e8-9ddf-6793c7698b0b}</Project>
<Name>Wino.Core</Name>
</ProjectReference>
<ProjectReference Include="..\Wino.Messages\Wino.Messaging.csproj">
<Project>{0c307d7e-256f-448c-8265-5622a812fbcc}</Project>
<Name>Wino.Messaging</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<SDKReference Include="WindowsDesktop, Version=10.0.22621.0">
<Name>Windows Desktop Extensions for the UWP</Name>
</SDKReference>
</ItemGroup>
<ItemGroup />
<PropertyGroup Condition=" '$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' &lt; '14.0' ">
<VisualStudioVersion>14.0</VisualStudioVersion>
</PropertyGroup>