Implement desktop sync protocol handling
This commit is contained in:
@@ -5,6 +5,8 @@ VisualStudioVersion = 17.0.31903.59
|
|||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WirelessTextSyncer.Windows", "WirelessTextSyncer.Windows\WirelessTextSyncer.Windows.csproj", "{7AD6081A-F4EB-45AA-917B-2CCFA54E0A67}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WirelessTextSyncer.Windows", "WirelessTextSyncer.Windows\WirelessTextSyncer.Windows.csproj", "{7AD6081A-F4EB-45AA-917B-2CCFA54E0A67}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WirelessTextSyncer.Windows.Tests", "WirelessTextSyncer.Windows.Tests\WirelessTextSyncer.Windows.Tests.csproj", "{FD53F0F1-EAE0-4CB2-9AC8-57797F0B95D1}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -18,5 +20,9 @@ Global
|
|||||||
{7AD6081A-F4EB-45AA-917B-2CCFA54E0A67}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{7AD6081A-F4EB-45AA-917B-2CCFA54E0A67}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{7AD6081A-F4EB-45AA-917B-2CCFA54E0A67}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{7AD6081A-F4EB-45AA-917B-2CCFA54E0A67}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{7AD6081A-F4EB-45AA-917B-2CCFA54E0A67}.Release|Any CPU.Build.0 = Release|Any CPU
|
{7AD6081A-F4EB-45AA-917B-2CCFA54E0A67}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{FD53F0F1-EAE0-4CB2-9AC8-57797F0B95D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{FD53F0F1-EAE0-4CB2-9AC8-57797F0B95D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{FD53F0F1-EAE0-4CB2-9AC8-57797F0B95D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{FD53F0F1-EAE0-4CB2-9AC8-57797F0B95D1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
31
WirelessTextSyncer.Windows.Tests/SyncMessageTests.cs
Normal file
31
WirelessTextSyncer.Windows.Tests/SyncMessageTests.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using WirelessTextSyncer.Windows.Models;
|
||||||
|
|
||||||
|
namespace WirelessTextSyncer.Windows.Tests;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public sealed class SyncMessageTests
|
||||||
|
{
|
||||||
|
[TestMethod]
|
||||||
|
public void DeserializeAndroidInsertTextMessage()
|
||||||
|
{
|
||||||
|
var message = JsonSerializer.Deserialize<SyncMessage>(
|
||||||
|
"""{"action":"insertText","text":"hello"}""",
|
||||||
|
new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||||
|
|
||||||
|
Assert.IsNotNull(message);
|
||||||
|
Assert.AreEqual(SyncAction.InsertText, message.Action);
|
||||||
|
Assert.AreEqual("hello", message.Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void DeserializeAndroidBackspaceMessage()
|
||||||
|
{
|
||||||
|
var message = JsonSerializer.Deserialize<SyncMessage>(
|
||||||
|
"""{"action":"backspace"}""",
|
||||||
|
new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||||
|
|
||||||
|
Assert.IsNotNull(message);
|
||||||
|
Assert.AreEqual(SyncAction.Backspace, message.Action);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||||
|
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
|
||||||
|
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\WirelessTextSyncer.Windows\WirelessTextSyncer.Windows.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
namespace WirelessTextSyncer.Windows.Models;
|
namespace WirelessTextSyncer.Windows.Models;
|
||||||
|
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
[JsonConverter(typeof(SyncActionJsonConverter))]
|
||||||
public enum SyncAction
|
public enum SyncAction
|
||||||
{
|
{
|
||||||
InsertText,
|
InsertText,
|
||||||
|
|||||||
41
WirelessTextSyncer.Windows/Models/SyncActionJsonConverter.cs
Normal file
41
WirelessTextSyncer.Windows/Models/SyncActionJsonConverter.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace WirelessTextSyncer.Windows.Models;
|
||||||
|
|
||||||
|
public sealed class SyncActionJsonConverter : JsonConverter<SyncAction>
|
||||||
|
{
|
||||||
|
public override SyncAction Read(
|
||||||
|
ref Utf8JsonReader reader,
|
||||||
|
Type typeToConvert,
|
||||||
|
JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
var value = reader.GetString();
|
||||||
|
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
"insertText" => SyncAction.InsertText,
|
||||||
|
"backspace" => SyncAction.Backspace,
|
||||||
|
"enter" => SyncAction.Enter,
|
||||||
|
"ping" => SyncAction.Ping,
|
||||||
|
_ => throw new JsonException($"Unsupported sync action: {value}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(
|
||||||
|
Utf8JsonWriter writer,
|
||||||
|
SyncAction value,
|
||||||
|
JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
var action = value switch
|
||||||
|
{
|
||||||
|
SyncAction.InsertText => "insertText",
|
||||||
|
SyncAction.Backspace => "backspace",
|
||||||
|
SyncAction.Enter => "enter",
|
||||||
|
SyncAction.Ping => "ping",
|
||||||
|
_ => throw new JsonException($"Unsupported sync action: {value}")
|
||||||
|
};
|
||||||
|
|
||||||
|
writer.WriteStringValue(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,15 @@
|
|||||||
namespace WirelessTextSyncer.Windows.Services;
|
namespace WirelessTextSyncer.Windows.Services;
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
public sealed class KeyboardInjectionService : IKeyboardInjectionService
|
public sealed class KeyboardInjectionService : IKeyboardInjectionService
|
||||||
{
|
{
|
||||||
|
private const uint InputKeyboard = 1;
|
||||||
|
private const uint KeyEventFKeyUp = 0x0002;
|
||||||
|
private const uint KeyEventFUnicode = 0x0004;
|
||||||
|
private const ushort VirtualKeyBack = 0x08;
|
||||||
|
private const ushort VirtualKeyReturn = 0x0D;
|
||||||
|
|
||||||
public void InsertText(string text)
|
public void InsertText(string text)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(text))
|
if (string.IsNullOrEmpty(text))
|
||||||
@@ -9,16 +17,103 @@ public sealed class KeyboardInjectionService : IKeyboardInjectionService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
SendKeys.SendWait(text);
|
foreach (var character in text)
|
||||||
|
{
|
||||||
|
SendUnicode(character);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Backspace()
|
public void Backspace()
|
||||||
{
|
{
|
||||||
SendKeys.SendWait("{BACKSPACE}");
|
SendVirtualKey(VirtualKeyBack);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Enter()
|
public void Enter()
|
||||||
{
|
{
|
||||||
SendKeys.SendWait("{ENTER}");
|
SendVirtualKey(VirtualKeyReturn);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SendUnicode(char character)
|
||||||
|
{
|
||||||
|
Send([
|
||||||
|
CreateUnicodeInput(character, 0),
|
||||||
|
CreateUnicodeInput(character, KeyEventFKeyUp)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SendVirtualKey(ushort virtualKey)
|
||||||
|
{
|
||||||
|
Send([
|
||||||
|
CreateVirtualKeyInput(virtualKey, 0),
|
||||||
|
CreateVirtualKeyInput(virtualKey, KeyEventFKeyUp)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Send(INPUT[] inputs)
|
||||||
|
{
|
||||||
|
var sent = SendInput((uint)inputs.Length, inputs, Marshal.SizeOf<INPUT>());
|
||||||
|
if (sent != inputs.Length)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Windows did not accept all keyboard input events.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static INPUT CreateUnicodeInput(char character, uint flags)
|
||||||
|
{
|
||||||
|
return new INPUT
|
||||||
|
{
|
||||||
|
type = InputKeyboard,
|
||||||
|
U = new InputUnion
|
||||||
|
{
|
||||||
|
ki = new KEYBDINPUT
|
||||||
|
{
|
||||||
|
wScan = character,
|
||||||
|
dwFlags = KeyEventFUnicode | flags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static INPUT CreateVirtualKeyInput(ushort virtualKey, uint flags)
|
||||||
|
{
|
||||||
|
return new INPUT
|
||||||
|
{
|
||||||
|
type = InputKeyboard,
|
||||||
|
U = new InputUnion
|
||||||
|
{
|
||||||
|
ki = new KEYBDINPUT
|
||||||
|
{
|
||||||
|
wVk = virtualKey,
|
||||||
|
dwFlags = flags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("user32.dll", SetLastError = true)]
|
||||||
|
private static extern uint SendInput(uint numberOfInputs, INPUT[] inputs, int sizeOfInputStructure);
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct INPUT
|
||||||
|
{
|
||||||
|
public uint type;
|
||||||
|
public InputUnion U;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Explicit)]
|
||||||
|
private struct InputUnion
|
||||||
|
{
|
||||||
|
[FieldOffset(0)]
|
||||||
|
public KEYBDINPUT ki;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct KEYBDINPUT
|
||||||
|
{
|
||||||
|
public ushort wVk;
|
||||||
|
public ushort wScan;
|
||||||
|
public uint dwFlags;
|
||||||
|
public uint time;
|
||||||
|
public UIntPtr dwExtraInfo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,14 @@ namespace WirelessTextSyncer.Windows.Services;
|
|||||||
|
|
||||||
public sealed class WebSocketServerService : IDisposable
|
public sealed class WebSocketServerService : IDisposable
|
||||||
{
|
{
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
};
|
||||||
|
|
||||||
private readonly SyncMessageHandler messageHandler;
|
private readonly SyncMessageHandler messageHandler;
|
||||||
private readonly List<IWebSocketConnection> clients = [];
|
private readonly List<IWebSocketConnection> clients = [];
|
||||||
|
private readonly object clientsLock = new();
|
||||||
private WebSocketServer? server;
|
private WebSocketServer? server;
|
||||||
|
|
||||||
public WebSocketServerService(SyncMessageHandler messageHandler)
|
public WebSocketServerService(SyncMessageHandler messageHandler)
|
||||||
@@ -21,7 +27,16 @@ public sealed class WebSocketServerService : IDisposable
|
|||||||
|
|
||||||
public string LocalIpAddress { get; private set; } = "127.0.0.1";
|
public string LocalIpAddress { get; private set; } = "127.0.0.1";
|
||||||
|
|
||||||
public bool HasClient => clients.Count > 0;
|
public bool HasClient
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (clientsLock)
|
||||||
|
{
|
||||||
|
return clients.Count > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public event EventHandler? StatusChanged;
|
public event EventHandler? StatusChanged;
|
||||||
|
|
||||||
@@ -34,13 +49,21 @@ public sealed class WebSocketServerService : IDisposable
|
|||||||
server.Start(socket =>
|
server.Start(socket =>
|
||||||
{
|
{
|
||||||
socket.OnOpen = () =>
|
socket.OnOpen = () =>
|
||||||
|
{
|
||||||
|
lock (clientsLock)
|
||||||
{
|
{
|
||||||
clients.Add(socket);
|
clients.Add(socket);
|
||||||
|
}
|
||||||
|
|
||||||
StatusChanged?.Invoke(this, EventArgs.Empty);
|
StatusChanged?.Invoke(this, EventArgs.Empty);
|
||||||
};
|
};
|
||||||
socket.OnClose = () =>
|
socket.OnClose = () =>
|
||||||
|
{
|
||||||
|
lock (clientsLock)
|
||||||
{
|
{
|
||||||
clients.Remove(socket);
|
clients.Remove(socket);
|
||||||
|
}
|
||||||
|
|
||||||
StatusChanged?.Invoke(this, EventArgs.Empty);
|
StatusChanged?.Invoke(this, EventArgs.Empty);
|
||||||
};
|
};
|
||||||
socket.OnMessage = HandleRawMessage;
|
socket.OnMessage = HandleRawMessage;
|
||||||
@@ -49,18 +72,26 @@ public sealed class WebSocketServerService : IDisposable
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
foreach (var client in clients.ToArray())
|
IWebSocketConnection[] currentClients;
|
||||||
|
lock (clientsLock)
|
||||||
|
{
|
||||||
|
currentClients = clients.ToArray();
|
||||||
|
clients.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var client in currentClients)
|
||||||
{
|
{
|
||||||
client.Close();
|
client.Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
clients.Clear();
|
|
||||||
server?.Dispose();
|
server?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleRawMessage(string rawMessage)
|
private void HandleRawMessage(string rawMessage)
|
||||||
{
|
{
|
||||||
var message = JsonSerializer.Deserialize<SyncMessage>(rawMessage);
|
try
|
||||||
|
{
|
||||||
|
var message = JsonSerializer.Deserialize<SyncMessage>(rawMessage, JsonOptions);
|
||||||
if (message is null)
|
if (message is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -68,6 +99,11 @@ public sealed class WebSocketServerService : IDisposable
|
|||||||
|
|
||||||
messageHandler.Handle(message);
|
messageHandler.Handle(message);
|
||||||
}
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static string ResolveLocalIpAddress()
|
private static string ResolveLocalIpAddress()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ public sealed class TrayApplicationContext : ApplicationContext
|
|||||||
private void UpdateTrayText()
|
private void UpdateTrayText()
|
||||||
{
|
{
|
||||||
var status = server.HasClient ? "connected" : "waiting";
|
var status = server.HasClient ? "connected" : "waiting";
|
||||||
notifyIcon.Text = $"WirelessTextSyncer {server.LocalIpAddress}:{server.Port} ({status})";
|
var text = $"WirelessTextSyncer {server.LocalIpAddress}:{server.Port} ({status})";
|
||||||
|
notifyIcon.Text = text.Length > 63 ? text[..63] : text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Fleck" Version="1.2.0" />
|
<PackageReference Include="Fleck" Version="1.2.0" />
|
||||||
<PackageReference Include="InputSimulatorPlus" Version="1.0.7" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.8" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.8" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user