Measuring User’s Activity level

In 2017, during a meeting with a client, a government agency, we discussed a project we developed and offered them, (in C++, obviously…), they asked us if it was possible to add log entries about a PC user’s activity level. That gave us the idea to add such a feature and in this post, I am going to show you how we can assign a dedicated thread to measure your level of activity while using your PC. That is, in fact, the final exercise of Chapter 15, Multithreading the Needle.

When you wish to monitor the activity level of the user, you would need a separate thread for
that. In our example, we will start the program and constantly display a gauge as a User
Interface element to help us provide that feedback.

Here is the source code, which is compatible with Windows and Mac OSX.

#include <iostream>
#include <atomic>
#include <chrono>
#ifdef _WIN32
#include <Windows.h>
#elif defined(__APPLE__)
#include <ApplicationServices/ApplicationServices.h>
#else
#error Unsupported platform
#endif
// Constants for activity level gauge
constexpr int MinActivityLevel = 0;
constexpr int MaxActivityLevel = 10;
constexpr int GaugeWidth = 20;
constexpr int InactivityThreshold = 5; // Number of seconds of inactivity before decrementing activity level

// Atomic variable to hold the current activity level
std::atomic<int> activityLevel(0);
std::chrono::time_point<std::chrono::system_clock> lastActivityTime;

// Low-level keyboard hook procedure
#ifdef _WIN32
LRESULT CALLBACK KeyboardHookProc(int nCode, WPARAM wParam, LPARAM lParam)
{
    if (nCode >= 0)
    {
        // Increase the activity level when a key is pressed
        if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN)
        {
            activityLevel = std::min<int>(activityLevel.load() + 1, MaxActivityLevel);
            lastActivityTime = std::chrono::system_clock::now(); // Update last activity time
        }
        else if (wParam == WM_KEYUP || wParam == WM_SYSKEYUP)
        {
            activityLevel = std::max<int>(activityLevel.load() - 0.5, MinActivityLevel);
            lastActivityTime = std::chrono::system_clock::now(); // Update last activity time
        }
    }

    // Call the next hook procedure in the hook chain
    return CallNextHookEx(nullptr, nCode, wParam, lParam);
}
#elif defined(__APPLE)
{
    CGEventRef KeyboardEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void* refcon)
    {
        if (type == kCGEventKeyDown || type == kCGEventFlagsChanged)
        {
            activityLevel = std::min<int>(activityLevel.load() + 1, MaxActivityLevel);
            lastActivityTime = std::chrono::system_clock::now(); // Update last activity time
        }
        else if (type == kCGEventKeyUp)
        {
            activityLevel = std::max<int>(activityLevel.load() - 0.5, MinActivityLevel);
            lastActivityTime = std::chrono::system_clock::now(); // Update last activity time
        }

        return event;
    }
}
#endif
// Low-level mouse hook procedure
#ifdef _WIN32
LRESULT CALLBACK MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam)
{
    if (nCode >= 0)
    {
        // Increase the activity level when the mouse moves
        if (wParam == WM_MOUSEMOVE)
        {
            activityLevel = std::min<int>(activityLevel.load() + 0.5, MaxActivityLevel);
            lastActivityTime = std::chrono::system_clock::now(); // Update last activity time
        }
        else if (wParam == WM_LBUTTONDOWN || wParam == WM_RBUTTONDOWN || wParam == WM_MBUTTONDOWN)
        {
            activityLevel = std::min<int>(activityLevel.load() + 1, MaxActivityLevel);
            lastActivityTime = std::chrono::system_clock::now(); // Update last activity time
        }
    }

    // Call the next hook procedure in the hook chain
    return CallNextHookEx(nullptr, nCode, wParam, lParam);
}
#elif defined (__APPLE__)
CGEventRef MouseEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void* refcon)
{
    if (type == kCGEventMouseMoved || type == kCGEventLeftMouseDown || type == kCGEventRightMouseDown)
    {
        activityLevel = std::min<int>(activityLevel.load() + 1, MaxActivityLevel);
        lastActivityTime = std::chrono::system_clock::now(); // Update last activity time
    }
    else if (type == kCGEventLeftMouseUp || type == kCGEventRightMouseUp)
    {
        activityLevel = std::max<int>(activityLevel.load() - 0.5, MinActivityLevel);
        lastActivityTime = std::chrono::system_clock::now(); // Update last activity time
    }

    return event;
}
#endif


// Function to calculate activity level based on keyboard and mouse events
void calculateActivityLevel()
{
#ifdef _WIN32

    // Set low-level keyboard hook
    HHOOK keyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, KeyboardHookProc, nullptr, 0);
    if (!keyboardHook)
    {
        std::cerr << "Failed to set keyboard hook." << std::endl;
        return;
    }

    // Set low-level mouse hook
    HHOOK mouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseHookProc, nullptr, 0);
    if (!mouseHook)
    {
        std::cerr << "Failed to set mouse hook." << std::endl;
        return;
    }

    // Message loop to keep the hooks active
    MSG msg;
    while (GetMessage(&msg, nullptr, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    // Unhook the keyboard
        // Unhook the keyboard hook
    UnhookWindowsHookEx(keyboardHook);

    // Unhook the mouse hook
    UnhookWindowsHookEx(mouseHook);

#elif defined( __APPLE__)
    // Create the event tap for keyboard events
    CGEventMask keyboardEventMask = CGEventMaskBit(kCGEventKeyDown) | CGEventMaskBit(kCGEventKeyUp) | CGEventMaskBit(kCGEventFlagsChanged);
    CFMachPortRef keyboardEventTap = CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault, keyboardEventMask, KeyboardEventCallback, nullptr);
    if (!keyboardEventTap)
    {
        std::cerr << "Failed to create keyboard event tap." << std::endl;
        return;
    }

    // Create a run loop source from the keyboard event tap
    CFRunLoopSourceRef keyboardRunLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, keyboardEventTap, 0);
    if (!keyboardRunLoopSource)
    {
        std::cerr << "Failed to create keyboard run loop source." << std::endl;
        CFRelease(keyboardEventTap);
        return;
    }

    // Add the keyboard run loop source to the current run loop
    CFRunLoopAddSource(CFRunLoopGetCurrent(), keyboardRunLoopSource, kCFRunLoopDefaultMode);

    // Enable the keyboard event tap
    CGEventTapEnable(keyboardEventTap, true);

    // Create the event tap for mouse events
    CGEventMask mouseEventMask = CGEventMaskBit(kCGEventMouseMoved) | CGEventMaskBit(kCGEventLeftMouseDown) | CGEventMaskBit(kCGEventRightMouseDown) | CGEventMaskBit(kCGEventLeftMouseUp) | CGEventMaskBit(kCGEventRightMouseUp);
    CFMachPortRef mouseEventTap = CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault, mouseEventMask, MouseEventCallback, nullptr);
    if (!mouseEventTap)
    {
        std::cerr << "Failed to create mouse event tap." << std::endl;
        return;
    }

    // Create a run loop source from the mouse event tap
    CFRunLoopSourceRef mouseRunLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, mouseEventTap, 0);
    if (!mouseRunLoopSource)
    {
        std::cerr << "Failed to create mouse run loop source." << std::endl;
        CFRelease(mouseEventTap);
        return;
    }

    // Add the mouse run loop source to the current run loop
    CFRunLoopAddSource(CFRunLoopGetCurrent(), mouseRunLoopSource, kCFRunLoopDefaultMode);

    // Enable the mouse event tap
    CGEventTapEnable(mouseEventTap, true);

    // Run the current run loop
    CFRunLoopRun();
#endif
}


// Function to display the color gradient gauge for a given activity level
// Function to display the color gradient gauge for a given activity level
void displayColorGradientGauge(int activityLevel)
{
    const int GaugeWidth = 20;

    // Define the color codes for gradient display
    const std::string ResetColor = "\033[0m";

    // Calculate the number of filled and empty cells for the activity level gauge
    int filledCells = std::round(static_cast<double>(GaugeWidth * activityLevel) / 10);
    int emptyCells = GaugeWidth - filledCells;

    // Display the activity level gauge
    std::cout << "[";

    // Display the filled cells with color gradient
    for (int i = 0; i < filledCells; ++i)
    {
        double percentage = static_cast<double>(i) / (GaugeWidth - 1);
        if (percentage <= 0.5)
        {
            // Gradient from light green to yellow
            int greenValue = static_cast<int>(255 - 128 * percentage / 0.5);
            int redValue = static_cast<int>(255 * percentage / 0.5);
            std::cout << "\033[38;2;" << redValue << ";" << greenValue << ";" << 0 << "m#";
        }
        else
        {
            // Gradient from yellow to red
            int redValue = 255;
            int greenValue = static_cast<int>(255 * (1.0 - percentage) / 0.5);
            std::cout << "\033[38;2;" << redValue << ";" << greenValue << ";" << 0 << "m#";
        }
    }

    // Display the empty cells in the default color
    std::cout << ResetColor;
    for (int i = 0; i < emptyCells; ++i)
    {
        std::cout << "-";
    }

    std::cout << "]" << std::endl;
}
// Function to display the activity gauge on the console
void displayActivityGauge()
{
    int previousLevel = -1; // Store the previous activity level
    lastActivityTime = std::chrono::system_clock::now();

    while (true)
    {
        int currentLevel = activityLevel.load(); // Read the current activity level

        // Check for inactivity
        std::chrono::time_point<std::chrono::system_clock> currentTime = std::chrono::system_clock::now();
        std::chrono::duration<double> elapsedTime = currentTime - lastActivityTime;
        if (elapsedTime.count() >= InactivityThreshold)
        {
            activityLevel = std::max<int>(activityLevel.load() - 1, MinActivityLevel);
            lastActivityTime = currentTime; // Update last activity time
        }

        // Clear the console
#ifdef _WIN32
        system("cls");
#elif defined(__APPLE__)
        system("clear");
#endif

        // Display the activity level gauge
        std::cout << "System Activity Level Gauge" << std::endl;
        displayColorGradientGauge(currentLevel);
        previousLevel = currentLevel; // Update the previous activity level

        // Sleep
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}


int main()
{
  // Start the activity level calculation thread
  std::thread calculationThread(calculateActivityLevel);
  // Start the activity gauge display thread
  std::thread displayThread(displayActivityGauge);

  // Wait for the threads to finish
  calculationThread.join();
  displayThread.join();

  return 0;
}

Unknown's avatar

About Michael Haephrati מיכאל האפרתי

Michael Haephrati is a music composer, an inventor and an expert specializes in software development and information security, who has built a unique perspective which combines technology and the end user experience. Author of Learning C++ https://www.manning.com/books/learning-c-plus-plus
This entry was posted in Uncategorized and tagged , , , , , , , , . Bookmark the permalink.

Leave a comment