You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
AutoHotInterception/C#/AutoHotInterception/Manager.cs

586 lines
23 KiB
C#

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<int, dynamic>
ContextCallbacks = new ConcurrentDictionary<int, dynamic>();
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<int, bool> FilteredDevices = new ConcurrentDictionary<int, bool>();
private static readonly ConcurrentDictionary<int, IDeviceHandler> DeviceHandlers = new ConcurrentDictionary<int, IDeviceHandler>();
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);
}
/// <summary>
/// Used by AHK code to make sure it can communicate with AHI
/// </summary>
/// <returns></returns>
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
/// <summary>
/// Subscribes to a Keyboard key
/// </summary>
/// <param name="id">The ID of the Keyboard</param>
/// <param name="code">The ScanCode of the key</param>
/// <param name="block">Whether or not to block the key</param>
/// <param name="callback">The callback to fire when the key changes state</param>
/// <param name="concurrent">Whether or not to execute callbacks concurrently</param>
/// <returns></returns>
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);
}
/// <summary>
/// Unsubscribe from a keyboard key
/// </summary>
/// <param name="id">The id of the keyboard</param>
/// <param name="code">The Scancode of the key</param>
public void UnsubscribeKey(int id, ushort code)
{
HelperFunctions.IsValidDeviceId(false, id);
SetFilterState(false);
var handler = DeviceHandlers[id];
handler.UnsubscribeSingleButton(code);
SetFilterState(true);
SetThreadState(true);
}
/// <summary>
/// Subscribe to all keys on a keyboard
/// </summary>
/// <param name="id">The id of the keyboard</param>
/// <param name="block">Whether or not to block the key</param>
/// <param name="callback">The callback to fire when the key changes state</param>
/// <param name="concurrent">Whether or not to execute callbacks concurrently</param>
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);
}
/// <summary>
/// Remove a SubscribeKeyboard subscription
/// </summary>
/// <param name="id">The id of the keyboard</param>
public void UnsubscribeKeyboard(int id)
{
HelperFunctions.IsValidDeviceId(false, id);
SetFilterState(false);
var handler = DeviceHandlers[id];
handler.UnsubscribeAllButtons();
SetFilterState(true);
SetThreadState(true);
}
/// <summary>
/// Subscribe to a specific button on a mouse
/// </summary>
/// <param name="id">The ID of the mouse</param>
/// <param name="code">The button number (LMB = 0, RMB = 1, MMB = 2, X1 = 3, X2 = 4, WV = 5, WH = 6)</param>
/// <param name="block">Whether or not to block the button</param>
/// <param name="callback">The callback to fire when the button changes state</param>
/// <param name="concurrent">Whether or not to execute callbacks concurrently</param>
/// <returns></returns>
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);
}
/// <summary>
/// Unsubscribes from a specific button on a mouse
/// </summary>
/// <param name="id">The ID of the mouse</param>
/// <param name="code">The button number (LMB = 0, RMB = 1, MMB = 2, X1 = 3, X2 = 4, WV = 5, WH = 6)</param>
public void UnsubscribeMouseButton(int id, ushort code)
{
HelperFunctions.IsValidDeviceId(true, id);
SetFilterState(false);
var handler = DeviceHandlers[id];
handler.UnsubscribeSingleButton(code);
SetFilterState(true);
SetThreadState(true);
}
/// <summary>
/// Create am AllButtons subscription for the specified mouse
/// </summary>
/// <param name="id">The ID of the mouse</param>
/// <param name="block">Whether or not to block the button</param>
/// <param name="callback">The callback to fire when the button changes state</param>
/// <param name="concurrent">Whether or not to execute callbacks concurrently</param>
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);
}
/// <summary>
/// Remove an AllButtons subscription for the specified mouse
/// </summary>
/// <param name="id">The ID of the mouse</param>
public void UnsubscribeMouseButtons(int id)
{
HelperFunctions.IsValidDeviceId(true, id);
SetFilterState(false);
var handler = DeviceHandlers[id];
handler.UnsubscribeAllButtons();
SetFilterState(true);
SetThreadState(true);
}
/// <summary>
/// Subscribes to Absolute mouse movement
/// </summary>
/// <param name="id">The id of the Mouse</param>
/// <param name="block">Whether or not to block the movement</param>
/// <param name="callback">The callback to fire when the mouse moves</param>
/// <param name="concurrent">Whether or not to execute callbacks concurrently</param>
/// <returns></returns>
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);
}
/// <summary>
/// Unsubscribes from absolute mouse movement
/// </summary>
/// <param name="id">The id of the mouse</param>
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);
}
/// <summary>
/// Subscribes to Relative mouse movement
/// </summary>
/// <param name="id">The id of the Mouse</param>
/// <param name="block">Whether or not to block the movement</param>
/// <param name="callback">The callback to fire when the mouse moves</param>
/// <param name="concurrent">Whether or not to execute callbacks concurrently</param>
/// <returns></returns>
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);
}
/// <summary>
/// Unsubscribes from relative mouse movement
/// </summary>
/// <param name="id">The id of the mouse</param>
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
/// <summary>
/// Sets a callback for Context Mode for a given device
/// </summary>
/// <param name="id">The ID of the device</param>
/// <param name="callback">The callback to fire before and after each key or button press</param>
/// <returns></returns>
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);
}
/// <summary>
/// Removes Context Mode for a given device
/// </summary>
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
/// <summary>
/// Sends a keyboard key event
/// </summary>
/// <param name="id">The ID of the Keyboard to send as</param>
/// <param name="code">The ScanCode to send</param>
/// <param name="state">The State to send (1 = pressed, 0 = released)</param>
public void SendKeyEvent(int id, ushort code, int state)
{
HelperFunctions.IsValidDeviceId(false, id);
var device = (KeyboardHandler)DeviceHandlers[id];
device.SendKeyEvent(code, state);
}
/// <summary>
/// Sends Mouse button events
/// </summary>
/// <param name="id"></param>
/// <param name="btn">Button ID to send</param>
/// <param name="state">State of the button</param>
/// <returns></returns>
public void SendMouseButtonEvent(int id, int btn, int state)
{
HelperFunctions.IsValidDeviceId(true, id);
var device = (MouseHandler)DeviceHandlers[id];
device.SendMouseButtonEvent(btn, state);
}
/// <summary>
/// Same as <see cref="SendMouseButtonEvent" />, but sends button events in Absolute mode (with coordinates)
/// </summary>
/// <param name="id">ID of the mouse</param>
/// <param name="btn">Button ID to send</param>
/// <param name="state">State of the button</param>
/// <param name="x">X position</param>
/// <param name="y">Y position</param>
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);
}
/// <summary>
/// Sends Relative Mouse Movement
/// </summary>
/// <param name="id">The id of the mouse</param>
/// <param name="x">X movement</param>
/// <param name="y">Y movement</param>
/// <returns></returns>
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);
}
/// <summary>
/// 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
/// </summary>
/// <param name="id"></param>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
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);
}
/// <summary>
/// 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
/// </summary>
/// <returns></returns>
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);
}
}
}
/// <summary>
/// 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....
/// </summary>
/// <param name="device">The Interception ID of the device</param>
/// <returns></returns>
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;
int stroke1DeviceId;
int stroke2DeviceId;
while (!token.IsCancellationRequested)
{
var stroke = new ManagedWrapper.Stroke();
// 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 (ManagedWrapper.Receive(DeviceContext, stroke1DeviceId = ManagedWrapper.WaitWithTimeout(DeviceContext, 10), ref stroke, 1) > 0)
{
var strokes = new List<ManagedWrapper.Stroke>();
strokes.Add(stroke);
if (stroke1DeviceId < 11)
{
//Debug.WriteLine($"Stroke 1: {RenderStroke(stroke)}");
// If this is a keyboard stroke, then keep performing more Receives immediately with a timeout of 0...
// ... this is to check whether an extended stroke is waiting.
// Unfortunately, at this point, it's entirely possible that two single-stroke keys end up in strokes...
// ... or even 3 strokes or more (eg one single-stroke key followed by a two-stroke key)
//while ((stroke2DeviceId = ManagedWrapper.WaitWithTimeout(DeviceContext, 0)) == stroke1DeviceId)
while ((stroke2DeviceId = ManagedWrapper.WaitWithTimeout(DeviceContext, 0)) != 0)
{
ManagedWrapper.Receive(DeviceContext, stroke2DeviceId, ref stroke, 1);
strokes.Add(stroke);
//Debug.WriteLine($"Stroke {strokes.Count}: {RenderStroke(stroke)}");
}
// Loop through the list checking the first 2 indexes for valid "two-code" key combinations.
// If no combo is found, send index 0 on its way, remove it off the top of the list, repeat
while (strokes.Count > 0)
{
if (strokes.Count >= 2 && ScanCodeHelper.IsDoubleScanCode(new List<ManagedWrapper.Stroke> { strokes[0], strokes[1] }))
{
DeviceHandlers[stroke1DeviceId].ProcessStroke(new List<ManagedWrapper.Stroke> { strokes[0], strokes[1] });
strokes.RemoveRange(0, 2);
}
else
{
DeviceHandlers[stroke1DeviceId].ProcessStroke(new List<ManagedWrapper.Stroke> { strokes[0] });
strokes.RemoveAt(0);
}
}
}
else
{
DeviceHandlers[stroke1DeviceId].ProcessStroke(strokes);
}
}
}
_pollThreadRunning = false;
//Debug.WriteLine($"AHK| Poll Thread Ended");
}
#endregion
}
}