* 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.
317 lines
13 KiB
C#
317 lines
13 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|
|
}
|