Detecting idle time and activity with I/O Kit

Author
Jean-David Gadina
Copyright
© 2024 Jean-David Gadina - www.xs-labs.com - All Rights Reserved
License
This article is published under the terms of the FreeBSD Documentation License

It may be sometimes useful, from an application, to know if the user is currently interacting with the computer (or phone), or if he's away.
This article explains how to detect user's activity, on Mac OS X and on iOS.

I/O Kit

There's in no direct way, with Cocoa, to detect if the computer is idle.
Idle means no interaction of the user with the computer. No mouse move nor keyboard entry, etc. Actions made solely by the OS are not concerned.

The OS has of course access to that information, to allow a screensaver to activate, or to initiate computer sleep.

To access this information, we'll have to use I/O Kit.
It consist in a collection of frameworks, libraries and tools used mainly to develop drivers for hardware components.

In our case, we are going to use IOKitLib, a library that allows programmers to access hardware resources through the Mac OS kernel.

As it's a low-level library, we need to code in C to use it.
So we are going to wrap the C code in an Objective-C class, to allow an easier and generic usage, as the C code may be harder to code for some programmers.

Project configuration

Before writing the actual code, we are going to configure our XCode project, so it can use IOKitLib.
As it's a library, it must be linked with the final binary.

Let's add a framework to our project:

For a Mac OS X application, we can choose «IOKit.framework».
For iOS, this framework is not available, so we must choose «libIOKit.dylib».

The framework is added to our project, and will now be linked with the application, after compilation time.


IOKitLib usage

First of all, here are the reference manuals for I/O Kit:

Now let's create an Objective-C class that will detect the idle time:

#include <IOKit/IOKitLib.h>

@interface IdleTime: NSObject
{
    mach_port_t   _ioPort;
    io_iterator_t _ioIterator;
    io_object_t   _ioObject;
}

@property( readonly ) uint64_t   timeIdle;
@property( readonly ) NSUInteger secondsIdle;

@end

This class has three instance variables, which will be used to communicate with I/O Kit.
The variables' types are defined by the «IOKit/IOKitLib.h», which we are including.

We are also defining two properties, that we'll use to access the idle time. The first one in nanoseconds, the second one in seconds.

Here's the basic implementation of the class:

#include "IdleTime.h"

@implementation IdleTime

- ( id )init
{
    if( ( self = [ super init ] ) )
    {
        
    }
    
    return self;
}

- ( void )dealloc
{
    [ super dealloc ];
}

- ( uint64_t )timeIdle
{
    return 0;
}

- ( NSUInteger )secondsIdle
{
    uint64_t time;
    
    time = self.timeIdle;
    
    return ( NSUInteger )( time >> 30 );
}

@end

We've got an «init» method that we will use to establish the base communication with I/O Kit, a «dealloc» method that will free the allocated resources, and a getter method for each property.

The second method (secondsIdle) only takes the time in nanoseconds and converts it into seconds. To do so, we just have to divide the nano time by 10 raised to the power of 9. As we have integer values, a 30 right shift does exactly that, in a more efficient way.

Now let's concentrate to the «init» method, and let's establish communication with I/O Kit, to obtain hardware informations.

- ( id )init
{
    kern_return_t status;
    
    if( ( self = [ super init ] ) )
    {
        
    }
    
    return self;
}

We a declaring a variable of type «kern_status» that we'll use to check the status of the I/O Kit communication, and to manage errors.
The following code is inside the «if» statement:

status = IOMasterPort( MACH_PORT_NULL, &_ioPort );

Here, we establish the connection with I/O Kit, on the default port (MACH_PORT_NULL).

To know if the operation was successfull, we can check the value of the status variable with «KERN_SUCCESS»:

if( status != KERN_SUCCESS )
{
    /* Error management... */
}

I/O Kit has many services. The one we are going to use is «IOHID». It will allow us to know about user interaction.
In the following code, we get an iterator on the I/O Kit services, so we can access to IOHID.

status = IOServiceGetMatchingServices
(
    _ioPort,
     IOServiceMatching( "IOHIDSystem" ),
    &_ioIterator
);

Now we can store the IOHID service:

_ioObject = IOIteratorNext( _ioIterator );

if ( ioObject == 0 )
{
    /* Error management */
}

IOObjectRetain( _ioObject );
IOObjectRetain( _ioIterator );

Here, we are doing a retain, so the objects won't be automatically freed.
So we'll have to release then in the «dealloc» method:

- ( void )dealloc
{
    IOObjectRelease( _ioObject );
    IOObjectRelease( _ioIterator );
    
    [ super dealloc ];
}

Now the I/O Kit communication is established, and we have access to IOHID.
We can now use that service in the «timeIdle» method.

- ( uint64_t )timeIdle
{
    kern_return_t          status;
    CFTypeRef              idle;
    CFTypeID               type;
    uint64_t               time;
    CFMutableDictionaryRef properties;
    
    properties = NULL;

Let's start by declaring the variables we are going to use.

First of all, we are going to access the IOHID properties.

status = IORegistryEntryCreateCFProperties
(
   _ioObject,
   &properties,
   kCFAllocatorDefault,
   0
);

Here, we get a dictionary (similar to NSDictionary) in the «properties» variable.
We also get a kernel status, that we have to check, as usual.

Now we can get the IOHID properties. The one we'll used is called «HIDIdleTime»:

idle = CFDictionaryGetValue( properties, CFSTR( "HIDIdleTime" ) );
    
if( !idle )
{
    CFRelease( ( CFTypeRef )properties );
    
    /* Error management */
}

If an error occurs, we have to release the «properties» object, in order to avoid a memory leak.

A dictionary can contains several types of values, so we have to know the type of the «HIDIdleTime» property, before using it.

type = CFGetTypeID( idle );

The property can be of type «number» or «data». To obtain the correct value, each case must be managed.

if( type == CFDataGetTypeID() )
{
    CFDataGetBytes( ( CFDataRef )idle, CFRangeMake( 0, sizeof( time ) ), ( UInt8 * )&time );
    
}
else if( type == CFNumberGetTypeID() )
{
    CFNumberGetValue( ( CFNumberRef )idle, kCFNumberSInt64Type, &time );
}
else
{
    CFRelease( idle );
    CFRelease( ( CFTypeRef )properties );
    
    /* Error management */
}

Then we can release the objects, and return the value:

CFRelease( idle );
CFRelease( ( CFTypeRef )properties );

return time;

The class is done. To use it, we just have to instantiate it and read the «secondsIdle» property (from a timer, for instance).

Demo

Here's an example program using that class to display the idle time:

To compile and execute it:

gcc -Wall -framework Cocoa -framework IOKit -o idle idle.m && ./idle