using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Threading; using AutoHotInterception.DeviceHandlers; using AutoHotInterception.Helpers; namespace AutoHotInterception { public class Manager : IDisposable { private static readonly ConcurrentDictionary ContextCallbacks = new ConcurrentDictionary(); private static readonly IntPtr DeviceContext = ManagedWrapper.CreateContext(); // If a device ID exists as a key in this Dictionary then that device is filtered. // Used by IsMonitoredDevice, which is handed to Interception as a "Predicate". //private static readonly ConcurrentDictionary FilteredDevices = new ConcurrentDictionary(); private static readonly ConcurrentDictionary DeviceHandlers = new ConcurrentDictionary(); private static bool _pollThreadRunning; private CancellationTokenSource _cancellationToken; #region Public #region Initialization public Manager() { for (int i = 1; i < 11; i++) { DeviceHandlers.TryAdd(i, new KeyboardHandler(DeviceContext, i)); } for (int i = 11; i < 21; i++) { DeviceHandlers.TryAdd(i, new MouseHandler(DeviceContext, i)); } } public void Dispose() { SetState(false); } /// /// Used by AHK code to make sure it can communicate with AHI /// /// public string OkCheck() { return "OK"; } public void SetState(bool state) { // Turn off the filter before turning off the thread... // .. this is to give the PollThread a chance to finish processing any buffered input SetFilterState(state); SetThreadState(state); } #endregion #region Subscription Mode /// /// Subscribes to a Keyboard key /// /// The ID of the Keyboard /// The ScanCode of the key /// Whether or not to block the key /// The callback to fire when the key changes state /// Whether or not to execute callbacks concurrently /// public void SubscribeKey(int id, ushort code, bool block, dynamic callback, bool concurrent = false) { HelperFunctions.IsValidDeviceId(false, id); SetFilterState(false); var handler = DeviceHandlers[id]; handler.SubscribeSingleButton(code, new MappingOptions { Block = block, Concurrent = concurrent, Callback = callback }); SetFilterState(true); SetThreadState(true); } /// /// Unsubscribe from a keyboard key /// /// The id of the keyboard /// The Scancode of the key public void UnsubscribeKey(int id, ushort code) { HelperFunctions.IsValidDeviceId(false, id); SetFilterState(false); var handler = DeviceHandlers[id]; handler.UnsubscribeSingleButton(code); SetFilterState(true); SetThreadState(true); } /// /// Subscribe to all keys on a keyboard /// /// The id of the keyboard /// Whether or not to block the key /// The callback to fire when the key changes state /// Whether or not to execute callbacks concurrently public void SubscribeKeyboard(int id, bool block, dynamic callback, bool concurrent = false) { HelperFunctions.IsValidDeviceId(false, id); SetFilterState(false); var handler = DeviceHandlers[id]; handler.SubscribeAllButtons(new MappingOptions { Block = block, Concurrent = concurrent, Callback = callback }); SetFilterState(true); SetThreadState(true); } /// /// Remove a SubscribeKeyboard subscription /// /// The id of the keyboard public void UnsubscribeKeyboard(int id) { HelperFunctions.IsValidDeviceId(false, id); SetFilterState(false); var handler = DeviceHandlers[id]; handler.UnsubscribeAllButtons(); SetFilterState(true); SetThreadState(true); } /// /// Subscribe to a specific button on a mouse /// /// The ID of the mouse /// The button number (LMB = 0, RMB = 1, MMB = 2, X1 = 3, X2 = 4, WV = 5, WH = 6) /// Whether or not to block the button /// The callback to fire when the button changes state /// Whether or not to execute callbacks concurrently /// public void SubscribeMouseButton(int id, ushort code, bool block, dynamic callback, bool concurrent = false) { HelperFunctions.IsValidDeviceId(true, id); SetFilterState(false); var handler = DeviceHandlers[id]; handler.SubscribeSingleButton(code, new MappingOptions { Block = block, Concurrent = concurrent, Callback = callback }); SetFilterState(true); SetThreadState(true); } /// /// Unsubscribes from a specific button on a mouse /// /// The ID of the mouse /// The button number (LMB = 0, RMB = 1, MMB = 2, X1 = 3, X2 = 4, WV = 5, WH = 6) public void UnsubscribeMouseButton(int id, ushort code) { HelperFunctions.IsValidDeviceId(true, id); SetFilterState(false); var handler = DeviceHandlers[id]; handler.UnsubscribeSingleButton(code); SetFilterState(true); SetThreadState(true); } /// /// Create am AllButtons subscription for the specified mouse /// /// The ID of the mouse /// Whether or not to block the button /// The callback to fire when the button changes state /// Whether or not to execute callbacks concurrently public void SubscribeMouseButtons(int id, bool block, dynamic callback, bool concurrent = false) { HelperFunctions.IsValidDeviceId(true, id); SetFilterState(false); var handler = DeviceHandlers[id]; handler.SubscribeAllButtons(new MappingOptions { Block = block, Concurrent = concurrent, Callback = callback }); SetFilterState(true); SetThreadState(true); } /// /// Remove an AllButtons subscription for the specified mouse /// /// The ID of the mouse public void UnsubscribeMouseButtons(int id) { HelperFunctions.IsValidDeviceId(true, id); SetFilterState(false); var handler = DeviceHandlers[id]; handler.UnsubscribeAllButtons(); SetFilterState(true); SetThreadState(true); } /// /// Subscribes to Absolute mouse movement /// /// The id of the Mouse /// Whether or not to block the movement /// The callback to fire when the mouse moves /// Whether or not to execute callbacks concurrently /// public void SubscribeMouseMoveAbsolute(int id, bool block, dynamic callback, bool concurrent = false) { HelperFunctions.IsValidDeviceId(true, id); SetFilterState(false); var device = (MouseHandler)DeviceHandlers[id]; device.SubscribeMouseMoveAbsolute(new MappingOptions { Block = block, Concurrent = concurrent, Callback = callback }); SetFilterState(true); SetThreadState(true); } /// /// Unsubscribes from absolute mouse movement /// /// The id of the mouse public void UnsubscribeMouseMoveAbsolute(int id) { HelperFunctions.IsValidDeviceId(true, id); SetFilterState(false); var device = (MouseHandler)DeviceHandlers[id]; device.UnsubscribeMouseMoveAbsolute(); SetFilterState(true); SetThreadState(true); } //Shorthand for SubscribeMouseMoveRelative public void SubscribeMouseMove(int id, bool block, dynamic callback, bool concurrent = false) { SubscribeMouseMoveRelative(id, block, callback, concurrent); } public void UnsubscribeMouseMove(int id) { UnsubscribeMouseMoveRelative(id); } /// /// Subscribes to Relative mouse movement /// /// The id of the Mouse /// Whether or not to block the movement /// The callback to fire when the mouse moves /// Whether or not to execute callbacks concurrently /// public void SubscribeMouseMoveRelative(int id, bool block, dynamic callback, bool concurrent = false) { HelperFunctions.IsValidDeviceId(true, id); SetFilterState(false); var device = (MouseHandler)DeviceHandlers[id]; device.SubscribeMouseMoveRelative(new MappingOptions { Block = block, Concurrent = concurrent, Callback = callback }); SetFilterState(true); SetThreadState(true); } /// /// Unsubscribes from relative mouse movement /// /// The id of the mouse public void UnsubscribeMouseMoveRelative(int id) { HelperFunctions.IsValidDeviceId(true, id); SetFilterState(false); var device = (MouseHandler)DeviceHandlers[id]; device.UnsubscribeMouseMoveRelative(); SetFilterState(true); SetThreadState(true); } #endregion #region Context Mode /// /// Sets a callback for Context Mode for a given device /// /// The ID of the device /// The callback to fire before and after each key or button press /// public void SetContextCallback(int id, dynamic callback) { SetFilterState(false); if (id < 1 || id > 20) throw new ArgumentOutOfRangeException(nameof(id), "DeviceIds must be between 1 and 20"); var device = DeviceHandlers[id]; device.SetContextCallback(callback); SetFilterState(true); SetThreadState(true); } /// /// Removes Context Mode for a given device /// public void RemoveContextCallback(int id) { SetFilterState(false); if (id < 1 || id > 20) throw new ArgumentOutOfRangeException(nameof(id), "DeviceIds must be between 1 and 20"); if (id < 11) { var device = (KeyboardHandler)DeviceHandlers[id]; device.RemoveContextCallback(); } else { } SetFilterState(true); SetThreadState(true); } #endregion #region Input Synthesis /// /// Sends a keyboard key event /// /// The ID of the Keyboard to send as /// The ScanCode to send /// The State to send (1 = pressed, 0 = released) public void SendKeyEvent(int id, ushort code, int state) { HelperFunctions.IsValidDeviceId(false, id); var device = (KeyboardHandler)DeviceHandlers[id]; device.SendKeyEvent(code, state); } /// /// Sends Mouse button events /// /// /// Button ID to send /// State of the button /// public void SendMouseButtonEvent(int id, int btn, int state) { HelperFunctions.IsValidDeviceId(true, id); var device = (MouseHandler)DeviceHandlers[id]; device.SendMouseButtonEvent(btn, state); } /// /// Same as , but sends button events in Absolute mode (with coordinates) /// /// ID of the mouse /// Button ID to send /// State of the button /// X position /// Y position public void SendMouseButtonEventAbsolute(int id, int btn, int state, int x, int y) { HelperFunctions.IsValidDeviceId(true, id); var device = (MouseHandler)DeviceHandlers[id]; device.SendMouseButtonEventAbsolute(btn, state, x, y); } public void SendMouseMove(int id, int x, int y) { SendMouseMoveRelative(id, x, y); } /// /// Sends Relative Mouse Movement /// /// The id of the mouse /// X movement /// Y movement /// public void SendMouseMoveRelative(int id, int x, int y) { HelperFunctions.IsValidDeviceId(true, id); var stroke = new ManagedWrapper.Stroke { mouse = { x = x, y = y, flags = (ushort)ManagedWrapper.MouseFlag.MouseMoveRelative } }; ManagedWrapper.Send(DeviceContext, id, ref stroke, 1); } /// /// Sends Absolute Mouse Movement /// Note: Creating a new stroke seems to make Absolute input become relative to main monitor /// Calling Send on an actual stroke from an Absolute device results in input relative to all monitors /// /// /// /// /// public void SendMouseMoveAbsolute(int id, int x, int y) { HelperFunctions.IsValidDeviceId(true, id); var device = (MouseHandler)DeviceHandlers[id]; device.SendMouseMoveAbsolute(x, y); } #endregion #region Device Querying public int GetKeyboardId(int vid, int pid, int instance = 1) { return GetDeviceId(false, vid, pid, instance); } public int GetMouseId(int vid, int pid, int instance = 1) { return GetDeviceId(true, vid, pid, instance); } public int GetKeyboardIdFromHandle(string handle, int instance = 1) { return HelperFunctions.GetDeviceIdFromHandle(DeviceContext, false, handle, instance); } public int GetMouseIdFromHandle(string handle, int instance = 1) { return HelperFunctions.GetDeviceIdFromHandle(DeviceContext, true, handle, instance); } public int GetDeviceIdFromHandle(bool isMouse, string handle, int instance = 1) { return HelperFunctions.GetDeviceIdFromHandle(DeviceContext, isMouse, handle, instance); } public int GetDeviceId(bool isMouse, int vid, int pid, int instance = 1) { return HelperFunctions.GetDeviceId(DeviceContext, isMouse, vid, pid, instance); } /// /// Gets a list of connected devices /// Intended to be used called via the AHK wrapper... /// ... so it can convert the return value into an AHK array /// /// public HelperFunctions.DeviceInfo[] GetDeviceList() { return HelperFunctions.GetDeviceList(DeviceContext); } #endregion #endregion Public #region Private private void SetThreadState(bool state) { if (state && !_pollThreadRunning) { _cancellationToken = new CancellationTokenSource(); ThreadPool.QueueUserWorkItem(PollThread, _cancellationToken.Token); while (!_pollThreadRunning) { // Wait for PollThread to actually start Thread.Sleep(10); } } else if (!state && _pollThreadRunning) { _cancellationToken.Cancel(); _cancellationToken.Dispose(); while (_pollThreadRunning) { // Wait for PollThread to actually stop Thread.Sleep(10); } } } /// /// Predicate used by Interception to decide whether to filter this device or not. /// WARNING! Setting this to always return true is RISKY, as you could lock yourself out of Windows... /// ... requiring a reboot. /// When working with AHI, it's generally best to keep this matching as little as possible.... /// /// The Interception ID of the device /// private static int IsMonitoredDevice(int device) { return DeviceHandlers[device].IsFiltered(); } private void SetFilterState(bool state) { ManagedWrapper.SetFilter(DeviceContext, IsMonitoredDevice, state ? ManagedWrapper.Filter.All : ManagedWrapper.Filter.None); } private static string RenderStroke(ManagedWrapper.Stroke stroke) { return $"key code: {stroke.key.code}, key state: {stroke.key.state}, mouse x/y: {stroke.mouse.x}, {stroke.mouse.y}"; } private static void PollThread(object obj) { var token = (CancellationToken)obj; //Debug.WriteLine($"AHK| Poll Thread Started"); _pollThreadRunning = true; var stroke1 = new ManagedWrapper.Stroke(); var stroke2 = new ManagedWrapper.Stroke(); int stroke1DeviceId; int stroke2DeviceId; //bool newPoll = true; while (!token.IsCancellationRequested) { // While no input happens, this loop will exit every 10ms to allow us to check if cancellation has been requested // WaitWithTimeout is used with a timeout of 10ms instead of Wait, so that when we eg use SetState to turn the thread off... // ... any input which was filtered and is waiting to be processed can be processed (eg lots of mouse moves buffered) //if (newPoll) //{ // Debug.WriteLine($"\n\n\nNEXT POLL"); // newPoll = false; //} if (ManagedWrapper.Receive(DeviceContext, stroke1DeviceId = ManagedWrapper.WaitWithTimeout(DeviceContext, 10), ref stroke1, 1) > 0) { //newPoll = true; var strokes = new List(); //Debug.WriteLine($"Stroke: {RenderStroke(stroke1)}"); //if (stroke1.key.code == 83 && stroke1.key.state == 2) //{ // throw new Exception("Saw second character of Del two-stroke press sequence when expecting a first stroke"); //} strokes.Add(stroke1); if (stroke1DeviceId < 11) { // If this is a keyboard stroke, then perform another Receive immediately with a timeout of 0... // ... this is to check whether an extended stroke is waiting if (ManagedWrapper.Receive(DeviceContext, stroke2DeviceId = ManagedWrapper.WaitWithTimeout(DeviceContext, 0), ref stroke2, 1) > 0) { if (stroke2DeviceId != stroke1DeviceId) { // Never seems to happen, but conceivably possible throw new Exception("Stroke 2 DeviceId is not the same as Stroke 1 DeviceId"); } //Debug.WriteLine($"Second stroke: {RenderStroke(stroke2)}"); strokes.Add(stroke2); } } DeviceHandlers[stroke1DeviceId].ProcessStroke(strokes); } } _pollThreadRunning = false; //Debug.WriteLine($"AHK| Poll Thread Ended"); } #endregion } }