Detecting User Inactivity/Idle Time in Xamarin.iOS using async/await / by Pranav Khandelwal

iOS includes a method within the UIApplication class that can be used to disable the idle timer and keep the device from going into idle. This is often used during long running processes where the device needs to remain in an active state:

Objectice-C
[UIApplication sharedApplication].idleTimerDisabled = YES;

Xamarin.iOS
UIApplication.SharedApplication.IdleTimerDisabled = True;

What is missing is a hook to determine how long a user has been idle while the application is in an active state. After searching for code snippets on how to accomplish this, I noticed that there were many in Objective-C but none for Xamarin, C#. Furthermore, the Objective-C ones made use of NSTimer, which was something I wanted to avoid. 

I decided to go the async/await route and dispatch a thread that would return after a specified amount of time. One of the requirements was that we must restart the timer every time the user touches the screen, which was a perfect fit for CancellationTokens.

See the implementation here:

Step 1: Add the following 3 methods to your AppDelegate:

		private CancellationTokenSource idleTimerCancellationTokenSource;
        public void ResetIdleTimer ()
		{
    		//use, then gid rid of the old CancellationTokenSource
			if (idleTimerCancellationTokenSource != null) {
				idleTimerCancellationTokenSource.Cancel ();
				idleTimerCancellationTokenSource.Dispose ();
				idleTimerCancellationTokenSource = null;
			}
				// Restart the timer with a new CancellationTokenSource
				StartIdleTimer (new CancellationTokenSource ());
		}

		public void StopIdleTimer ()
		{
        	// use, then get rid of the CancellationTokenSource
			if (idleTimerCancellationTokenSource != null){
				idleTimerCancellationTokenSource.Cancel ();
                idleTimerCancellationTokenSource.Dispose ();
				idleTimerCancellationTokenSource = null;
             }
		}
		
		public async Task StartIdleTimer (CancellationTokenSource tokenSource)
		{
			try {
            	//maintain a reference to the token so we can cancel when needed
				idleTimerCancellationTokenSource = tokenSource;

				Debug.WriteLine ("Idle Timer Thread Started");
				await Task.Delay (TimeSpan.FromSeconds(120), tokenSource.Token);

				Debug.WriteLine ("Idle Timeout Detected, Do Stuff!");
                //Do something here, like show a screensaver or something
                InvokeOnMainThread(() => /* Do Some navigation or something */);

			} catch (TaskCanceledException ex) {
            	//if we cancel/reset, this catch block gets called
				Debug.WriteLine ("Idle Timer Thread Cancelled");
			}
			// if we reach here, this timer has stopped
			Debug.WriteLine ("Idle Timer Thread Complete");
		}

Step 2: Create a custom UIApplication and override the SendEvent(UIEvent uievent) method to capture application-wide Touch events. Call ResetIdleTimer() in the AppDelegate when a touch event is captured:

    public class CustomApplication : UIApplication
    {
        public CustomApplication () : base ()
        {
        }

        public CustomApplication (IntPtr handle) : base (handle)
        {
        }

        public CustomApplication (Foundation.NSObjectFlag t) : base (t)
        {
        }
        public override void SendEvent (UIEvent uievent)
        {
            if (uievent.Type == UIEventType.Touches) {
                if (uievent.AllTouches.Cast<UITouch> ().Any (t => t.Phase == UITouchPhase.Began)) {
                    ((AppDelegate)Delegate).ResetIdleTimer ();
                }
            }

            base.SendEvent (uievent);
        }
    }

Step 3: Update Main.cs to use our new CustomApplication class instead of the default UIApplication class:

static void Main (string[] args)
{
    UIApplication.Main (args, typeof(CustomApplication), typeof(AppDelegate));
}

Step 4: To start/stop the Idle Timer, you can make the following calls from anywhere in your iOS code:

START:
((AppDelegate)UIApplication.SharedApplication.Delegate).StartIdleTimer (new CancellationTokenSource ());

STOP:
((AppDelegate)UIApplication.SharedApplication.Delegate).StopIdleTimer ();

Remember, that anything UI specific that is called when the Timeout is reached should be wrapped in an InvokeOnMainThread(...):

InvokeOnMainThread(() => /* Do Some navigation or something */);

And thats it! Congratulations! You have implemented an Idle Timer that will run in the background and be reset each time the user has any on screen activity.

Feel free to reach out if you have any questions or suggestions on improving this code! I am planning on creating an open soruce cross-platform helper for this and posting to GitHub. Check back for the link!

Thanks!
PK