Search code examples
c#visual-studiopinvokevisual-studio-extensions

Detect application-specific user activity within Visual Studio Extension (VSIX)


I would like to programmatically detect when a user is interacting with Visual Studio from within a Visual Studio extension (VSIX). I do not want any keypress or mouse event information, just a notification the user is doing something within the running Visual Studio instance the VSIX is installed in. I seek a 'signal' to know if the user is moving the mouse across the VS IDE, or clicking any mouse button, or pressing any key or key combination that the running VS instance will 'see'.

Why do I wish to know this? I have an inactivity timer running which will trigger some behaviour if inactivity meets or exceeds a defined period. I need to be able to reset the start time of the timer when the user performs any form of keyboard/mouse input before such. This is not limited to code editor windows, but literally any form of user interaction with the running Visual Studio instance. So equally it is not Windows system-wide, but it is beyond the extension (VSIX) itself.

Currently, I am using system-wide hooks and trying to filter the event messages to Visual Studio main window handle as obtained from the EnvDTE80 object. This seems far too intensive and results in excess windows messages needing some kind of evaluation, with a periodic side effect of slowing responsiveness of the entire environment. (It's not doing a Garbage Collection when the system slows).

Here is a sanitized/simplified gist of what I am using:

using System;
using System.Runtime.InteropServices;

public class UserActivityMonitor
{
    [DllImport("user32.dll")]
    private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelInputProc callback, IntPtr hMod, uint dwThreadId);

    [DllImport("user32.dll")]
    private static extern bool UnhookWindowsHookEx(IntPtr hhk);

    [DllImport("user32.dll")]
    private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

    private delegate IntPtr LowLevelInputProc(int nCode, IntPtr wParam, IntPtr lParam);

    private const int WH_KEYBOARD_LL = 13;
    private const int WH_MOUSE_LL = 14;

    private const int WM_KEYDOWN = 0x0100;
    private const int WM_LBUTTONDOWN = 0x0201;
    private const int WM_RBUTTONDOWN = 0x0204;
    private const int WM_MOUSEMOVE = 0x0200;

    private static IntPtr keyboardHookID = IntPtr.Zero;
    private static IntPtr mouseHookID = IntPtr.Zero;

    public void StartMonitoring()
    {
        // Set up the keyboard hook
        keyboardHookID = SetWindowsHookEx(WH_KEYBOARD_LL, HookKeyboardCallback, IntPtr.Zero, 0);

        // Set up the mouse hook
        mouseHookID = SetWindowsHookEx(WH_MOUSE_LL, HookMouseCallback, IntPtr.Zero, 0);
    }

    public void StopMonitoring()
    {
        // Unhook the keyboard hook
        UnhookWindowsHookEx(keyboardHookID);

        // Unhook the mouse hook
        UnhookWindowsHookEx(mouseHookID);
    }

    private IntPtr HookKeyboardCallback(int nCode, IntPtr wParam, IntPtr lParam)
    {
        if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN)
        {
            // Handle keyboard event - reset the start time to now (not shown) 
        }
        return CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam);
    }

    private IntPtr HookMouseCallback(int nCode, IntPtr wParam, IntPtr lParam)
    {
        if (nCode >= 0 && (wParam == (IntPtr)WM_LBUTTONDOWN || wParam == (IntPtr)WM_RBUTTONDOWN || wParam == (IntPtr)WM_MOUSEMOVE))
        {
            // Handle mouse move event - reset the start time to now (not shown) 
        }
        return CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam);
    }
}

The difference to my actual proprietary code is that where I have code in the hook callbacks, I also try to determine the window handle from lParam and compare this to Visual Studio's main window handle obtained by using an EnvDTE80 reference, so that essentially I will only reset my inactivity timer for valid user interactions with the running Visual Studio instance the extension is installed.

I'd like a higher-level way of achieving the same, ideally avoiding P/Invoke calls and using managed code only.


Solution

  • I moved away from this approach, in favour of simply asking the system for the idle time each second, based off Detecting User Activity

    Each second when my timer elapses I make a simple call to retrieve a timespan for the user's inactivity period (which could theoretically be zero).

    private TimeSpan GetElapsedIdleTime()
    {
        LASTINPUTINFO lastInputInfo = new LASTINPUTINFO();
        lastInputInfo.cbSize = (uint)System.Runtime.InteropServices.Marshal.SizeOf(lastInputInfo);
        GetLastInputInfo(ref lastInputInfo);
        long ticksElapsed = ((long)Environment.TickCount - (long)lastInputInfo.dwTime);
        return new TimeSpan(ticksElapsed);
    }
    

    I still cannot detect idleness with respect to Visual Studio only, for instance the user could be busy using another application, but from Visual Studio's perspective the user has not interacted with it for more than the configured inactivity period. But this solution will do for now.

    To solve this, I plan to track when Visual Studio is the top-most window, and start a separate timer when it isn't, which if it reaches the configured inactivity period will trigger the action I want. Likewise, when Visual Studio is switched to and becomes the top-most window, I will stop this secondary timer.