//----------------------------------------------------------------------------
//
//  Microsoft Windows
//  Copyright (C) Microsoft Corporation, 1997
//
//  File:       msidle.cpp
//
//  Contents:   user idle detection
//
//  Classes:
//
//  Functions:
//
//  History:    05-14-1997  darrenmi (Darren Mitchell) Created
//
//----------------------------------------------------------------------------

#include <nt.h>
#include <ntrtl.h>
#include <nturtl.h>
#include <windows.h>
#include "msidle.h"
#include "resource.h"

// useful things...
#ifndef ARRAYSIZE
#define ARRAYSIZE(a) (sizeof(a) / sizeof(a[0]))
#endif

//
// Global unshared variables
//
DWORD   g_dwIdleMin = 0;                // inactivity minutes before idle
UINT_PTR g_uIdleTimer = 0;              // idle timer for this process
BOOL    g_fIdleNotify = FALSE;          // notify when idle
BOOL    g_fBusyNotify = FALSE;          // notify when busy
BOOL    g_fIsWinNT = FALSE;             // which platform?
BOOL    g_fIsWinNT5 = FALSE;            // are we running on NT5?
BOOL    g_fIsWhistler = FALSE;          // are we running on Whistler?
HANDLE  g_hSageVxd = INVALID_HANDLE_VALUE;
                                        // handle to sage.vxd
DWORD   g_dwIdleBeginTicks = 0;         // ticks when we became idle
HINSTANCE g_hInst = NULL;               // dll instance
_IDLECALLBACK g_pfnCallback = NULL;     // function to call back in client

#ifdef MSIDLE_DOWNLEVEL
//
// Global shared variables
//
#pragma data_seg(".shrdata")

HHOOK   sg_hKbdHook = NULL, sg_hMouseHook = NULL;
DWORD   sg_dwLastTickCount = 0;
POINT   sg_pt = {0,0};

#pragma data_seg()

//
// Prototypes
//
LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam);
LRESULT CALLBACK KbdProc(int nCode, WPARAM wParam, LPARAM lParam);
#endif // MSIDLE_DOWNLEVEL

VOID CALLBACK OnIdleTimer(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime);

//
// From winuser.h, but NT5 only
//
#if (_WIN32_WINNT < 0x0500)
typedef struct tagLASTINPUTINFO {
    UINT cbSize;
    DWORD dwTime;
} LASTINPUTINFO, * PLASTINPUTINFO;
#endif

//
// NT5 api we dynaload from user32
//
typedef WINUSERAPI BOOL (WINAPI* PFNGETLASTINPUTINFO)(PLASTINPUTINFO plii);

PFNGETLASTINPUTINFO pfnGetLastInputInfo = NULL;

///////////////////////////////////////////////////////////////////////////
//
//                     Internal functions
//
///////////////////////////////////////////////////////////////////////////

#ifdef DEBUG

BOOL ReadRegValue(HKEY hkeyRoot, const TCHAR *pszKey, const TCHAR *pszValue, 
                   void *pData, DWORD dwBytes)
{
    long    lResult;
    HKEY    hkey;
    DWORD   dwType;

    lResult = RegOpenKey(hkeyRoot, pszKey, &hkey);
    if (lResult != ERROR_SUCCESS) {
        return FALSE;
    }

    lResult = RegQueryValueEx(hkey, pszValue, NULL, &dwType, (BYTE *)pData, 
        &dwBytes);
    RegCloseKey(hkey);

    if (lResult != ERROR_SUCCESS) 
        return FALSE;
    
    if(dwType == REG_SZ) {
        // null terminate string
        ((TCHAR *)pData)[dwBytes] = 0;
    }

    return TRUE;
}

TCHAR *g_pszLoggingFile;
BOOL  g_fCheckedForLog = FALSE;

DWORD LogEvent(LPTSTR pszFormat, ...)
{

    // check registry if necessary
    if(FALSE == g_fCheckedForLog) {

        TCHAR   pszFilePath[MAX_PATH];

        if(ReadRegValue(HKEY_CURRENT_USER,
                TEXT("Software\\Microsoft\\Windows\\CurrentVersion\\msidle"),
                TEXT("LoggingFile"), pszFilePath, MAX_PATH)) {

            g_pszLoggingFile = (TCHAR *)LocalAlloc(LPTR, lstrlen(pszFilePath) + 1);
            if(g_pszLoggingFile) {
                lstrcpy(g_pszLoggingFile, pszFilePath);
            }
        }

        g_fCheckedForLog = TRUE;
    }

    if(g_pszLoggingFile) {

        TCHAR       pszString[1024];
        SYSTEMTIME  st;
        HANDLE      hLog;
        DWORD       dwWritten;
        va_list     va;

        hLog = CreateFile(g_pszLoggingFile, GENERIC_WRITE, 0, NULL,
                OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

        if(INVALID_HANDLE_VALUE == hLog)
            return GetLastError();

        // seek to end of file
        SetFilePointer(hLog, 0, 0, FILE_END);

        // dump time
        GetLocalTime(&st);
        wsprintf(pszString, "%02d:%02d:%02d [%x] - ", st.wHour, st.wMinute, st.wSecond, GetCurrentThreadId());
        WriteFile(hLog, pszString, lstrlen(pszString), &dwWritten, NULL);
        OutputDebugString(pszString);

        // dump passed in string
        va_start(va, pszFormat);
        wvsprintf(pszString, pszFormat, va);
        va_end(va);
        WriteFile(hLog, pszString, lstrlen(pszString), &dwWritten, NULL);
        OutputDebugString(pszString);

        // cr
        WriteFile(hLog, "\r\n", 2, &dwWritten, NULL);
        OutputDebugString("\r\n");

        // clean up
        CloseHandle(hLog);
    }

    return 0;
}

#endif // DEBUG

//
// SetIdleTimer - decide how often to poll and set the timer appropriately
//
void SetIdleTimer(void)
{
    UINT uInterval = 1000 * 60;

    //
    // If we're looking for loss of idle, check every 4 seconds
    //
    if(TRUE == g_fBusyNotify) {
        uInterval = 1000 * 4;
    }

    //
    // kill off the old timer
    //
    if(g_uIdleTimer) {
        KillTimer(NULL, g_uIdleTimer);
    }

    //
    // Set the timer
    //
    g_uIdleTimer = SetTimer(NULL, 0, uInterval, OnIdleTimer);
}
       
DWORD GetLastActivityTicks(void)
{
    DWORD dwLastActivityTicks = 0;

    if (g_fIsWhistler) {

        dwLastActivityTicks = USER_SHARED_DATA->LastSystemRITEventTickCount;

    } else if(g_fIsWinNT5 && pfnGetLastInputInfo) {
        // NT5: Use get last input time API
        LASTINPUTINFO lii;

        memset(&lii, 0, sizeof(lii));
        lii.cbSize = sizeof(lii);
        (*pfnGetLastInputInfo)(&lii);
        dwLastActivityTicks = lii.dwTime;
    } else {
        // NT4 or Win95: Use sage if it's loaded
        if(INVALID_HANDLE_VALUE != g_hSageVxd) {
            // query sage.vxd for tick count
            DeviceIoControl(g_hSageVxd, 2, &dwLastActivityTicks, sizeof(DWORD),
                NULL, 0, NULL, NULL);
        }
#ifdef MSIDLE_DOWNLEVEL
    else {
            // use hooks
            dwLastActivityTicks = sg_dwLastTickCount;
        }
#endif // MSIDLE_DOWNLEVEL
    }

    return dwLastActivityTicks;
}

//
// OnIdleTimer - idle timer has gone off
//
VOID CALLBACK OnIdleTimer(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime)
{
    DWORD   dwDiff, dwLastActivityTicks;
    BOOL    fTempBusyNotify = g_fBusyNotify;
    BOOL    fTempIdleNotify = g_fIdleNotify;

    //
    // get last activity ticks from sage or shared segment
    //
    dwLastActivityTicks = GetLastActivityTicks();

#ifdef DEBUG
    LogEvent("OnIdleTimer: dwLastActivity=%d, CurrentTicks=%d, dwIdleBegin=%d", dwLastActivityTicks, GetTickCount(), g_dwIdleBeginTicks);
#endif

    //
    // check to see if we've changed state
    //
    if(fTempBusyNotify) {
        //
        // Want to know if we become busy
        //
        if(dwLastActivityTicks != g_dwIdleBeginTicks) {
            // activity since we became idle - stop being idle!
            g_fBusyNotify = FALSE;
            g_fIdleNotify = TRUE;

            // set the timer
            SetIdleTimer();

            // call back client
#ifdef DEBUG
            LogEvent("OnIdleTimer: Idle Ends");
#endif
            if(g_pfnCallback)
                (g_pfnCallback)(STATE_USER_IDLE_END);
        }

    }

    if(fTempIdleNotify) {
        //
        // Want to know if we become idle
        //
        dwDiff = GetTickCount() - dwLastActivityTicks;

        if(dwDiff > 1000 * 60 * g_dwIdleMin) {
            // Nothing's happened for our threshold time.  We're now idle.
            g_fIdleNotify = FALSE;
            g_fBusyNotify = TRUE;

            // save time we became idle
            g_dwIdleBeginTicks = dwLastActivityTicks;

            // set the timer
            SetIdleTimer();

            // call back client
#ifdef DEBUG
            LogEvent("OnIdleTimer: Idle Begins");
#endif
            if(g_pfnCallback)
                (g_pfnCallback)(STATE_USER_IDLE_BEGIN);
        }
    }
}

BOOL LoadSageVxd(void)
{
    int inpVXD[3];

    if(INVALID_HANDLE_VALUE != g_hSageVxd)
        return TRUE;

    g_hSageVxd = CreateFile("\\\\.\\sage.vxd", 0, 0, NULL, 0,
            FILE_FLAG_DELETE_ON_CLOSE, NULL);

    // can't open it?  can't use it
    if(INVALID_HANDLE_VALUE == g_hSageVxd)
        return FALSE;

    // start it monitoring
    inpVXD[0] = -1;                         // no window - will query
    inpVXD[1] = 0;                          // unused
    inpVXD[2] = 0;                          // how long to wait between checks

    DeviceIoControl(g_hSageVxd, 1, &inpVXD, sizeof(inpVXD), NULL, 0, NULL, NULL);

    return TRUE;
}

BOOL UnloadSageVxd(void)
{
    if(INVALID_HANDLE_VALUE != g_hSageVxd) {
        CloseHandle(g_hSageVxd);
        g_hSageVxd = INVALID_HANDLE_VALUE;
    }

    return TRUE;
}

///////////////////////////////////////////////////////////////////////////
//
//                   Externally callable functions
//
///////////////////////////////////////////////////////////////////////////

//
// LibMain - dll entry point
//
EXTERN_C BOOL WINAPI LibMain(HINSTANCE hInst, ULONG ulReason, LPVOID pvRes)
{
    switch (ulReason) {
    case DLL_PROCESS_ATTACH:
        {
        OSVERSIONINFO vi;

        DisableThreadLibraryCalls(hInst);
        g_hInst = hInst;

        vi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
        GetVersionEx(&vi);
        if(vi.dwPlatformId == VER_PLATFORM_WIN32_NT) {
            g_fIsWinNT = TRUE;
            if(vi.dwMajorVersion >= 5) {
                if (vi.dwMajorVersion > 5 || vi.dwMinorVersion > 0 || LOWORD(vi.dwBuildNumber) > 2410) {
                    g_fIsWhistler = TRUE;
                } else {
                    g_fIsWinNT5 = TRUE;
                }
            }

        }
        }
        break;
    }

    return TRUE;
}

//
// BeginIdleDetection
//
DWORD BeginIdleDetection(_IDLECALLBACK pfnCallback, DWORD dwIdleMin, DWORD dwReserved)
{
    DWORD dwValue = 0;

    // make sure reserved is 0
    if(dwReserved)
        return ERROR_INVALID_DATA;

#ifdef DEBUG
    LogEvent("BeginIdleDetection: IdleMin=%d", dwIdleMin);
#endif

    // save callback
    g_pfnCallback = pfnCallback;

    // save minutes
    g_dwIdleMin = dwIdleMin;

    // call back on idle
    g_fIdleNotify = TRUE;

    if(FALSE == g_fIsWinNT) {
        // try to load sage.vxd
        LoadSageVxd();
    }

    if(g_fIsWinNT5) {
        // we need to find our NT5 api in user
        HINSTANCE hUser = GetModuleHandle("user32.dll");
        if(hUser) {
            pfnGetLastInputInfo =
                (PFNGETLASTINPUTINFO)GetProcAddress(hUser, "GetLastInputInfo");
        }

        if(NULL == pfnGetLastInputInfo) {
            // not on NT5 - bizarre
            g_fIsWinNT5 = FALSE;
        }
    }

#ifdef MSIDLE_DOWNLEVEL
    if(INVALID_HANDLE_VALUE == g_hSageVxd && FALSE == g_fIsWinNT5 && FALSE == g_fIsWhistler) {

        // sage vxd not available - do it the hard way

        // hook kbd
        sg_hKbdHook = SetWindowsHookEx(WH_KEYBOARD, KbdProc, g_hInst, 0);
        if(NULL == sg_hKbdHook)
            return GetLastError();
        
        // hook mouse
        sg_hMouseHook = SetWindowsHookEx(WH_MOUSE, MouseProc, g_hInst, 0);
        if(NULL == sg_hMouseHook) {
            DWORD dwError = GetLastError();
            EndIdleDetection(0);
            return dwError;
        }
    }
#endif // MSIDLE_DOWNLEVEL

    // Fire up the timer
    SetIdleTimer();

    return 0;
}

//
// IdleEnd - stop idle monitoring
//
BOOL EndIdleDetection(DWORD dwReserved)
{
    // ensure reserved is 0
    if(dwReserved)
        return FALSE;

    // free up sage if we're using it
    UnloadSageVxd();

    // kill timer
    if(g_uIdleTimer) {
        KillTimer(NULL, g_uIdleTimer);
        g_uIdleTimer = 0;
    }

    // callback is no longer valid
    g_pfnCallback = NULL;

#ifdef MSIDLE_DOWNLEVEL
    // free up hooks
    if(sg_hKbdHook) {
        UnhookWindowsHookEx(sg_hKbdHook);
        sg_hKbdHook = NULL;
    }

    if(sg_hMouseHook) {
        UnhookWindowsHookEx(sg_hMouseHook);
        sg_hMouseHook = NULL;
    }
#endif // MSIDLE_DOWNLEVEL

    return TRUE;
}

//
// SetIdleMinutes - set the timout value and reset idle flag to false
//
// dwMinutes   - if non-0, set idle timeout to that many minutes
// fIdleNotify - call back when idle for at least idle minutes
// fBusyNotify - call back on activity since Idle begin
//
BOOL SetIdleTimeout(DWORD dwMinutes, DWORD dwReserved)
{
    if(dwReserved)
        return FALSE;

#ifdef DEBUG
    LogEvent("SetIdleTimeout: dwIdleMin=%d", dwMinutes);
#endif

    if(dwMinutes)
        g_dwIdleMin = dwMinutes;

    return TRUE;
}

//
// SetIdleNotify - set flag to turn on or off idle notifications
//
// fNotify - flag
// dwReserved - must be 0
//
void SetIdleNotify(BOOL fNotify, DWORD dwReserved)
{
#ifdef DEBUG
    LogEvent("SetIdleNotify: fNotify=%d", fNotify);
#endif

    g_fIdleNotify = fNotify;
}

//
// SetIdleNotify - set flag to turn on or off idle notifications
//
// fNotify - flag
// dwReserved - must be 0
//
void SetBusyNotify(BOOL fNotify, DWORD dwReserved)
{
#ifdef DEBUG
    LogEvent("SetBusyNotify: fNotify=%d", fNotify);
#endif

    g_fBusyNotify = fNotify;

    if(g_fBusyNotify)
        g_dwIdleBeginTicks = GetLastActivityTicks();

    // set the timer
    SetIdleTimer();
}

//
// GetIdleMinutes - return how many minutes since last user activity
//
DWORD GetIdleMinutes(DWORD dwReserved)
{
    if(dwReserved)
        return 0;

    return (GetTickCount() - GetLastActivityTicks()) / 60000;
}

#ifdef MSIDLE_DOWNLEVEL
///////////////////////////////////////////////////////////////////////////
//
//                           Hook functions
//
///////////////////////////////////////////////////////////////////////////

//
// Note: These functions can be called back in any process!
//
LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
{
    MOUSEHOOKSTRUCT * pmsh = (MOUSEHOOKSTRUCT *)lParam;

    if(nCode >= 0) {
        // ignore mouse move messages to the same point as all window
        // creations cause these - it doesn't mean the user moved the mouse
        if(WM_MOUSEMOVE != wParam || pmsh->pt.x != sg_pt.x || pmsh->pt.y != sg_pt.y) {
            sg_dwLastTickCount = GetTickCount();
            sg_pt = pmsh->pt;
        }
    }

    return(CallNextHookEx(sg_hMouseHook, nCode, wParam, lParam));
}

LRESULT CALLBACK KbdProc(int nCode, WPARAM wParam, LPARAM lParam)
{
    if(nCode >= 0) {
        sg_dwLastTickCount = GetTickCount();
    }

    return(CallNextHookEx(sg_hKbdHook, nCode, wParam, lParam));
}
#endif // MSIDLE_DOWNLEVEL