diff --git a/WirelessTextSyncer.Desktop.sln b/WirelessTextSyncer.Desktop.sln index 9c3c6f0..84956a7 100644 --- a/WirelessTextSyncer.Desktop.sln +++ b/WirelessTextSyncer.Desktop.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WirelessTextSyncer.Windows", "WirelessTextSyncer.Windows\WirelessTextSyncer.Windows.csproj", "{7AD6081A-F4EB-45AA-917B-2CCFA54E0A67}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WirelessTextSyncer.Windows.Tests", "WirelessTextSyncer.Windows.Tests\WirelessTextSyncer.Windows.Tests.csproj", "{FD53F0F1-EAE0-4CB2-9AC8-57797F0B95D1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution 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}.Release|Any CPU.ActiveCfg = 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 EndGlobal diff --git a/WirelessTextSyncer.Windows.Tests/SyncMessageTests.cs b/WirelessTextSyncer.Windows.Tests/SyncMessageTests.cs new file mode 100644 index 0000000..ad5cdb9 --- /dev/null +++ b/WirelessTextSyncer.Windows.Tests/SyncMessageTests.cs @@ -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( + """{"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( + """{"action":"backspace"}""", + new JsonSerializerOptions(JsonSerializerDefaults.Web)); + + Assert.IsNotNull(message); + Assert.AreEqual(SyncAction.Backspace, message.Action); + } +} diff --git a/WirelessTextSyncer.Windows.Tests/WirelessTextSyncer.Windows.Tests.csproj b/WirelessTextSyncer.Windows.Tests/WirelessTextSyncer.Windows.Tests.csproj new file mode 100644 index 0000000..a3aabc8 --- /dev/null +++ b/WirelessTextSyncer.Windows.Tests/WirelessTextSyncer.Windows.Tests.csproj @@ -0,0 +1,27 @@ + + + + net8.0-windows + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/WirelessTextSyncer.Windows/Models/SyncAction.cs b/WirelessTextSyncer.Windows/Models/SyncAction.cs index b784cab..d7f0bee 100644 --- a/WirelessTextSyncer.Windows/Models/SyncAction.cs +++ b/WirelessTextSyncer.Windows/Models/SyncAction.cs @@ -1,5 +1,8 @@ namespace WirelessTextSyncer.Windows.Models; +using System.Text.Json.Serialization; + +[JsonConverter(typeof(SyncActionJsonConverter))] public enum SyncAction { InsertText, diff --git a/WirelessTextSyncer.Windows/Models/SyncActionJsonConverter.cs b/WirelessTextSyncer.Windows/Models/SyncActionJsonConverter.cs new file mode 100644 index 0000000..7b0712a --- /dev/null +++ b/WirelessTextSyncer.Windows/Models/SyncActionJsonConverter.cs @@ -0,0 +1,41 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace WirelessTextSyncer.Windows.Models; + +public sealed class SyncActionJsonConverter : JsonConverter +{ + 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); + } +} diff --git a/WirelessTextSyncer.Windows/Services/KeyboardInjectionService.cs b/WirelessTextSyncer.Windows/Services/KeyboardInjectionService.cs index 9e661dc..6d59ec7 100644 --- a/WirelessTextSyncer.Windows/Services/KeyboardInjectionService.cs +++ b/WirelessTextSyncer.Windows/Services/KeyboardInjectionService.cs @@ -1,7 +1,15 @@ namespace WirelessTextSyncer.Windows.Services; +using System.Runtime.InteropServices; + 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) { if (string.IsNullOrEmpty(text)) @@ -9,16 +17,103 @@ public sealed class KeyboardInjectionService : IKeyboardInjectionService return; } - SendKeys.SendWait(text); + foreach (var character in text) + { + SendUnicode(character); + } } public void Backspace() { - SendKeys.SendWait("{BACKSPACE}"); + SendVirtualKey(VirtualKeyBack); } 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()); + 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; } } diff --git a/WirelessTextSyncer.Windows/Services/WebSocketServerService.cs b/WirelessTextSyncer.Windows/Services/WebSocketServerService.cs index 5c6c38d..cdc96a2 100644 --- a/WirelessTextSyncer.Windows/Services/WebSocketServerService.cs +++ b/WirelessTextSyncer.Windows/Services/WebSocketServerService.cs @@ -8,8 +8,14 @@ namespace WirelessTextSyncer.Windows.Services; public sealed class WebSocketServerService : IDisposable { + private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web) + { + PropertyNameCaseInsensitive = true + }; + private readonly SyncMessageHandler messageHandler; private readonly List clients = []; + private readonly object clientsLock = new(); private WebSocketServer? server; public WebSocketServerService(SyncMessageHandler messageHandler) @@ -21,7 +27,16 @@ public sealed class WebSocketServerService : IDisposable 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; @@ -35,12 +50,20 @@ public sealed class WebSocketServerService : IDisposable { socket.OnOpen = () => { - clients.Add(socket); + lock (clientsLock) + { + clients.Add(socket); + } + StatusChanged?.Invoke(this, EventArgs.Empty); }; socket.OnClose = () => { - clients.Remove(socket); + lock (clientsLock) + { + clients.Remove(socket); + } + StatusChanged?.Invoke(this, EventArgs.Empty); }; socket.OnMessage = HandleRawMessage; @@ -49,24 +72,37 @@ public sealed class WebSocketServerService : IDisposable 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(); } - clients.Clear(); server?.Dispose(); } private void HandleRawMessage(string rawMessage) { - var message = JsonSerializer.Deserialize(rawMessage); - if (message is null) + try + { + var message = JsonSerializer.Deserialize(rawMessage, JsonOptions); + if (message is null) + { + return; + } + + messageHandler.Handle(message); + } + catch (JsonException) { return; } - - messageHandler.Handle(message); } private static string ResolveLocalIpAddress() diff --git a/WirelessTextSyncer.Windows/Tray/TrayApplicationContext.cs b/WirelessTextSyncer.Windows/Tray/TrayApplicationContext.cs index 38bdd2f..58f30ed 100644 --- a/WirelessTextSyncer.Windows/Tray/TrayApplicationContext.cs +++ b/WirelessTextSyncer.Windows/Tray/TrayApplicationContext.cs @@ -75,6 +75,7 @@ public sealed class TrayApplicationContext : ApplicationContext private void UpdateTrayText() { 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; } } diff --git a/WirelessTextSyncer.Windows/WirelessTextSyncer.Windows.csproj b/WirelessTextSyncer.Windows/WirelessTextSyncer.Windows.csproj index 1454a5c..05ec6e6 100644 --- a/WirelessTextSyncer.Windows/WirelessTextSyncer.Windows.csproj +++ b/WirelessTextSyncer.Windows/WirelessTextSyncer.Windows.csproj @@ -10,7 +10,6 @@ -