Mac / iPhone / iPad – Détecter l’inactivité de l’utilisateur avec I/O Kit

written by netinfluence 9 février 2010
Mac / iPhone / iPad – Détecter l’inactivité de l’utilisateur avec I/O Kit
Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.3 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. A copy of the license can be found at: http://www.gnu.org/copyleft/fdl.html
Il peut être parfois utile, dans une application, de savoir si l’utilisateur est en train d’intéragir avec son ordinateur (ou téléphone) ou s’il est parti boire un café.
Cet article explique comment détecter l’inactivité de l’utilisateur. Il s’applique aussi bien au développement pour Mac OS X que pour iPhone / iPad.

I/O Kit

Il n’existe à ma connaissance pas de moyen direct, en Cocoa, pour déterminer l’inactivité.
Par inactivité, il faut comprendre absence d’interaction de l’utilisateur avec la machine. L’interaction peut être le déplacement de la souris, une action sur le clavier, etc, mais non une action déclenchée uniquement par la machine.
Le système dispose bien entendu de cette information. C’est ce qui permet de déclencher un économiseur d’écran, ou de mettre la machine en veille.
Pour accéder à cette information, nous allons devoir utiliser I/O Kit.
Il s’agit d’une collection de frameworks, librairies et outils destinés principalement au développement de drivers pour des composants matériels.
Dans notre cas, nous allons utiliser plus principalement IOKitLib, une librairie permettant aux applications d’accéder aux ressources matérielles par le biais du kernel.
Comme il s’agit d’une librairie de (relativement) bas niveau, nous allons devoir coder en C pour l’utiliser.
Nous allons donc, pour des raison pratiques, et pour permettre une utilisation générique, créer une classe Objective-C encapsulant ce code C parfois un peu moins digeste pour certains développeurs Cocoa et iPhone.

Configuration du projet

Avant de commencer, nous allons configurer notre projet XCode, afin de permettre l’utilisation de IOKitLib.
En effet, puisqu’il s’agit d’une librairie, celle-ci doit être «linkée» à notre application finale.
Il suffit d’ajouter un framework à notre projet:
Pour une application Mac OS X, nous allons choisir « IOKit.framework ».
Pour iPhone et iPad, ce framework n’est pas disponible dans son ensemble. Dans un tel cas, il faudra choisir « libIOKit.dylib ».
Le framework est du coup ajouté à notre projet, et sera «linké» avec notre application après la compilation.

Utilisation d’IOKitLib

Avant toute chose, voici les documents de références d’I/O Kit:
Nous allons tout d’abord créer une classe Objective-C qui nous permettra de connaître l’inactivité:
#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

Cette classe contient 3 variables d’instances qui nous servirons à communiquer avec I/O Kit.
Les types correspondants à ces variables sont définis dans le fichier « IOKit/IOKitLib.h », que nous incluons.
Nous définissons également deux propriétés, qui nous servirons à accéder au temps inactif. La première en nanosecondes (ce que nous retournera I/O Kit), et la seconde en secondes (ce qui est souvent un peu plus pratique).
Voici l’implémentation basique de la classe:
#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

Nous avons une méthode « init », qui nous servira à établir la communication de base avec I/O Kit, une méthode « dealloc » qui nous permettra de libérer les ressources que nous avons alloués, et une méthode (getter) pour chacune de nos propriétés.
La seconde (secondsIdle) ne fait que prendre le temps en nanosecondes et le convertir en secondes. Pour ce faire, il suffit de diviser le temps par 10 puissance 9. Puisque nous avons des valeurs entières, un décalage de 30 sur la droite reviens exactement au même résultat (en plus rapide bien sûr).
Nous allons maintenant nous concentrer sur la méthode « init », et établir une communication avec I/O Kit, qui nous permettra d’obtenir des informations sur le matériel.
- ( id )init
{
kern_return_t status;

if( ( self = [ super init ] ) ) {

}

return self;
}
Nous déclarons une variable de type « kern_status », qui nous permettra de connaître le status de la communication avec I/O Kit, afin de gérer d’éventuelles erreurs.
La suite de code se passe à l’intérieur de l’instruction « if »:
status = IOMasterPort( MACH_PORT_NULL, &ioPort );
Ici, nous établissons la connexion avec I/O Kit, sur le port par défaut (MACH_PORT_NULL).
Pour contrôler le résultat de l’opération, nous pouvons comparer la valeur de « status » avec « KERN_SUCCESS »:
if( status != KERN_SUCCESS ) {

/* Error management... */
}
I/O Kit se compose de plusieurs services. Celui qui nous intéresse est « IOHID » (I/O Human Interface Driver). C’est celui qui nous permettra de connaître l’état de l’interaction entre l’utilisateur et la machine.
Nous récupérons dans le code suivant un itérateur sur les services I/O Kit, qui nous servira à accéder à IOHID.
status = IOServiceGetMatchingServices(
ioPort,
IOServiceMatching( "IOHIDSystem" ),
&ioIterator
);
Nous pouvons maintenant stocker notre service IOHID:
ioObject = IOIteratorNext( ioIterator );

if ( ioObject == 0 ) {

/* Error management */
}

IOObjectRetain( ioObject );
IOObjectRetain( ioIterator );

Nos effectuons ici un « retain », pour s’assurer que nos objets ne seront pas désalloués automatiquement.
Il ne faut donc pas oublier d’effectuer un « release » dans la méthode « dealloc »:
- ( void )dealloc
{
IOObjectRelease( ioObject );
IOObjectRelease( ioIterator );

[ super dealloc ];
}
Nous avons donc maintenant établi la communication avec I/O Kit, et obtenu un accès au service IOHID.
Il ne nous reste plus qu’à interroger ce service, dans la méthode « timeIdle ».
- ( uint64_t )timeIdle
{
kern_return_t status;
CFTypeRef idle;
CFTypeID type;
uint64_t time;
CFMutableDictionaryRef properties;

properties = NULL;
Nous déclarons ici les différentes variables que nous allons utiliser.
Nous allons en premier lieu récupérer les propriétés de IOHID.
status = IORegistryEntryCreateCFProperties(
ioObject,
&properties,
kCFAllocatorDefault,
0
);
Nous récupérons ici dans notre variable « properties » un dictionnaire, comparable à l’objet « NSDictionnary ».
Nous récupérons également toujours un status du kernel, qu’il nous faut vérifier comme précédemment.
Nous pouvons donc désormais obtenir un propriété de IOHID. Celle qui nous intéresse se nomme « HIDIdleTime »:
idle = CFDictionaryGetValue( properties, CFSTR( "HIDIdleTime" ) );

if( !idle ) {

CFRelease( ( CFTypeRef )properties );

/* Error management */
}
En cas d’erreur, il ne faut pas oublier de releaser l’objet « properties », afin d’éviter un memory leak.
Un dictionnaire peut contenir plusieurs types de valeurs. Nous devons donc connaître le type de la propriété « HIDIdleTime » avant de la traiter.
type = CFGetTypeID( idle );
La propriété peut être de type « number » ou « data ». Pour obtenir la valeur correcte, chaque cas doit être pris en compte.
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 */
}
Il ne reste plus qu’à releaser nos objets, et à retourner la valeur:
CFRelease( idle );
CFRelease( ( CFTypeRef )properties );

return time;

La classe est terminée. Pour l’utiliser, il suffit de l’instancier et de consulter sa propriété « secondsIdle » (depuis un timer par exemple).

Demo

Voici un exemple de programme utilisant cette classe pour afficher l’inactivité:
Pour le compiler et l’executer:
gcc -Wall -framework Cocoa -framework IOKit -o idle idle.m && ./idle

You may also like

2 comments

Romac 9 février 2010 at 19 h 44 min

Excellent article, comme toujours! Bravo!

PS: Ça fait tellement de bien de lire du code aussi bien formatté (surtout pour de l’Objective-C) 😉

Reply
Serge 3 août 2010 at 17 h 45 min

Merci d’avoir publié ce tutoriel. Je l’ai étudie afin d’utiliser dans une application que je suis en train d’écrire. Tout fonctionne bien, mais j’ai découvert quelques choses au sujet de ton code que j’aimerais partager.

D’abord, on peut se passer entièrement de l’ouverture du Master Port en spécifiant ‘kIOMasterPortDefault’ au lieu de la variable ‘ioPort’. Puis, ce n’est pas la peine d’utiliser ‘IOServiceGetMatchingServices’ pour obtenir une collection de services. On peut dire tout simplement ‘IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching(« IOHIDSystem »))’ pour obtenir une référence au service HID directement. On n’a pas besoin de l’iterator, alors.

En suite, je suggère ‘IORegistryEntryCreateCFProperty(ioService, CFSTR(« HIDIdleTime »), kCFAllocatorDefault, NULL)’ pour se passer d’un dictionnaire et accéder à la variable en question directement.

À mon avis, comme ça le code sera beaucoup plus efficace et clair. Si tu as de commentaires à propos de mes suggestions, je les aimerais savoir.

Encore une fois, merci et excuses-moi de mon français imparfait.
Serge.

Reply

Leave a Comment