Implement desktop sync protocol handling
This commit is contained in:
@@ -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
|
||||
|
||||
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;
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
[JsonConverter(typeof(SyncActionJsonConverter))]
|
||||
public enum SyncAction
|
||||
{
|
||||
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;
|
||||
|
||||
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<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
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
private readonly SyncMessageHandler messageHandler;
|
||||
private readonly List<IWebSocketConnection> 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<SyncMessage>(rawMessage);
|
||||
if (message is null)
|
||||
try
|
||||
{
|
||||
var message = JsonSerializer.Deserialize<SyncMessage>(rawMessage, JsonOptions);
|
||||
if (message is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
messageHandler.Handle(message);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
messageHandler.Handle(message);
|
||||
}
|
||||
|
||||
private static string ResolveLocalIpAddress()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Fleck" Version="1.2.0" />
|
||||
<PackageReference Include="InputSimulatorPlus" Version="1.0.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.8" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user