4

I wanted to make a function that would timeout and navigate to the main page if the user is idle for a certain period of time. After a little research, I found that the ThreadPoolTimer should suit my needs. Testing it I decided to use a 10 sec interval.

    timer =ThreadPoolTimer.CreatePeriodicTimer(Timer_Tick,TimeSpan.FromSeconds(10));

And this is where I'm at a loss. I couldn't figure out a way to check user input on a UWP without having to individually check PointerPressed, PointerExited, etc. So I did some more digging and I found a block of code that's supposed to give you a boolean value if the user is idle or not.

    public static uint GetIdleTime()
    {
        LASTINPUTINFO lastInPut = new LASTINPUTINFO();
        lastInPut.cbSize = (uint)Marshal.SizeOf(lastInPut);
        GetLastInputInfo(ref lastInPut);

        return ((uint)Environment.TickCount - lastInPut.dwTime);
    }

    public static bool IsUserIdle()
    {
        uint idleTime = (uint)Environment.TickCount - GetLastInputEventTickCount();
       if (idleTime > 0)
       {
           idleTime = (idleTime / 1000);
       }
       else
       {
           idleTime = 0;
       }
       //user is idle for 10 sec
       bool b = (idleTime >= 10);
       return b;
    }

    private static uint GetLastInputEventTickCount()
    {
        LASTINPUTINFO lii = new LASTINPUTINFO();
        lii.cbSize = (uint)Marshal.SizeOf(lii);
        lii.dwTime = 0;
        uint p = GetLastInputInfo(ref lii) ? lii.dwTime : 0;
        return p;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct LASTINPUTINFO
    {
        public static readonly int SizeOf = Marshal.SizeOf<LASTINPUTINFO>();
        [MarshalAs(UnmanagedType.U4)]
        public UInt32 cbSize;
        [MarshalAs(UnmanagedType.U4)]
        public UInt32 dwTime;
    }

    [DllImport("user32.dll")]
    private static extern bool GetLastInputInfo(ref LASTINPUTINFO plii);

I then call the function in the tick function and use the conditional statement if IsUserIdle() is equal to true then navigate to the main page.

    public static void Timer_Tick(object sender)
    {
       if (IsUserIdle() == true)
       {
           Frame.Navigate(typeof(MainPage));
       }                                        
    }

But when I start it nothing happens, and after I set a couple breakpoints I found that IsUserIdle() never returns a true value even after 10 sec of inactivity. I am completely stuck so any help would be appreciated.

Calvin Carter
  • 59
  • 1
  • 7

1 Answers1

10

GetLastInputInfo isn't supported for Windows store apps:

Minimum supported client: Windows 2000 Professional [desktop apps only]

I'm not aware of any intrinsic UWP API to detect if the user is idle, but it's definitely possible to whip up your own mechanism for doing so.

I couldn't figure out a way to check user input on a UWP without having to individually check PointerPressed, PointerExited, etc.

What's so bad about that approach? Here's my attempt:

App.xaml.cs

public sealed partial class App : Application
{
    public static new App Current => (App)Application.Current;

    public event EventHandler IsIdleChanged;

    private DispatcherTimer idleTimer;

    private bool isIdle;
    public bool IsIdle
    {
        get
        {
            return isIdle;
        }

        private set
        {
            if (isIdle != value)
            {
                isIdle = value;
                IsIdleChanged?.Invoke(this, EventArgs.Empty);
            }
        }
    }

    protected override void OnLaunched(LaunchActivatedEventArgs e)
    {
        idleTimer = new DispatcherTimer();
        idleTimer.Interval = TimeSpan.FromSeconds(10);  // 10s idle delay
        idleTimer.Tick += onIdleTimerTick;
        Window.Current.CoreWindow.PointerMoved += onCoreWindowPointerMoved;
    }

    private void onIdleTimerTick(object sender, object e)
    {
        idleTimer.Stop();
        IsIdle = true;
    }

    private void onCoreWindowPointerMoved(CoreWindow sender, PointerEventArgs args)
    {
        idleTimer.Stop();
        idleTimer.Start();
        IsIdle = false;
    }
}

MainPage.xaml.cs

public sealed partial class MainPage : Page
{
    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
        App.Current.IsIdleChanged += onIsIdleChanged;
    }

    protected override void OnNavigatedFrom(NavigationEventArgs e)
    {
        App.Current.IsIdleChanged -= onIsIdleChanged;
    }

    private void onIsIdleChanged(object sender, EventArgs e)
    {
        System.Diagnostics.Debug.WriteLine($"IsIdle: {App.Current.IsIdle}");
    }
}

Idle is detected when the pointer hasn't moved for 10s within the app window. This will work also for touch-only apps (like mobile apps) because PointerMoved will fire when the window is tapped, too.

Decade Moon
  • 32,968
  • 8
  • 81
  • 101
  • *"What's so bad about that approach?"* - In short: It doesn't deliver. It can only record input events for the calling process. And it doesn't distinguish between user input, and events that are the result of automation. This approach produces both false positives as well as false negatives, and certainly doesn't establish, whether the user has been idle. – IInspectable Dec 18 '16 at 22:34
  • Unfortunately there is no alternative (that I am aware of). This would be sufficient for most scenarios. – Decade Moon Dec 19 '16 at 01:11
  • This may or may not be sufficient. However, a reader of this proposed answer needs to know, where and how this implementation deviates from a solution based on `GetLastInputInfo`. As it stands, nothing in this answer even hints to any difference. – IInspectable Dec 19 '16 at 16:41
  • I've actually fixed my initial problem by using the dispatcher timer and tick function instead of the threadpool timer. It works perfectly when I debug but now when i try to run it in release mode i get a System.TypeLoad Exception saying that Unresolved P/Invoke method 'GetLastInputInfo!user32.dll' in assembly because it is not available in UWP applications. Please either use an another API , or use [DllImport(ExactSpelling=true) to indicate that you understand the implications of using non-UWP application APIs. @IInspectable – Calvin Carter Dec 19 '16 at 17:04
  • With that being said, @DecadeMoon could you please provide a more form fitting alternative? Like do I create another function using the dispatchertimer that navigates to a page based on whether or not IsIdle is true or not? Or do you just add it in the onIdleTimerTick function after IsIdle is set to true? – Calvin Carter Dec 19 '16 at 17:07
  • You won't be allowed to publish the app in the store if you use an unsupported API such as GetLastInputInfo. The only way that we know of to detect user idle is the method I have shown (or similar), however it is not a complete substitute because it only works based on input to your app window instead of a global system-detected idle (as IInspectable pointed out). Plus, it might need more functionality to handle things like suspend and resume. With my solution, you shouldn't need to change the App.xaml.cs code at all, just subscribe to the IsIdleChanged event in the context that makes sense. – Decade Moon Dec 19 '16 at 23:26
  • 1
    reporting. suggested answer is very stable on UWP windows IOT. upvoted. – Zen Of Kursat Nov 28 '17 at 09:07
  • what if i create a background process and the app is in the background? Can i still check if the user is idle? – Ali123 Aug 20 '19 at 12:32
  • I tried this solution and it works when you are active on the app only. I need a solution that will detect if user is idle on the whole device and not only the app – Ali123 Sep 15 '19 at 13:09