Objective C: Get notifications about a user's idle state
Asked Answered
Y

4

7

My cocoa app runs background tasks, which I would like to stop when the user becomes idle (no keyboard/mouse input) and then resume when the user becomes active again. Is there a way to register for idle-state notifications?

Yoruba answered 10/1, 2011 at 3:59 Comment(0)
C
6

In case you can't link to Carbon (ie. you want to compile x86_64 bit binary) you can wrap this function (which returns current idle time in seconds resolution as double - CFTimeInterval) in a timer:

#include <IOKit/IOKitLib.h>

CFTimeInterval CFDateGetIdleTimeInterval() {
    mach_port_t port;
    io_iterator_t iter;
    CFTypeRef value = kCFNull;
    uint64_t idle = 0;
    CFMutableDictionaryRef properties = NULL;
    io_registry_entry_t entry;

    IOMasterPort(MACH_PORT_NULL, &port);
    IOServiceGetMatchingServices(port, IOServiceMatching("IOHIDSystem"), &iter);
    if (iter) {
        if ((entry = IOIteratorNext(iter))) {
            if (IORegistryEntryCreateCFProperties(entry, &properties, kCFAllocatorDefault, 0) == KERN_SUCCESS && properties) {
                if (CFDictionaryGetValueIfPresent(properties, CFSTR("HIDIdleTime"), &value)) {
                    if (CFGetTypeID(value) == CFDataGetTypeID()) {
                        CFDataGetBytes(value, CFRangeMake(0, sizeof(idle)), (UInt8 *) &idle);
                    } else if (CFGetTypeID(value) == CFNumberGetTypeID()) {
                        CFNumberGetValue(value, kCFNumberSInt64Type, &idle);
                    }
                }
                CFRelease(properties);
            }
            IOObjectRelease(entry);
        }
        IOObjectRelease(iter);
    }

    return idle / 1000000000.0;
}

You'll need to link your code to IOKit.framework

Caron answered 6/1, 2012 at 9:34 Comment(2)
Small edit, entry = IOIteratorNext(tier) should be entry = IOIteratorNext(iter).Whitby
Please note that HIDIdleTime is not very reliable and it always returns zero on some Macs for unknown reasons (probably some connected devices reset the idle time count).Pic
U
6

There's a Carbon API that will send a notification when there hasn't been a user event after a certain duration called EventLoopIdleTimer. Uli Kusterer has written a Cocoa wrapper for here (look for UKIdleTimer).

If you want something lower level, you may be able to implement the behavior you want with a combination of timers and the CoreGraphics function CGEventSourceSecondsSinceLastEventType (available in <CoreGraphics/CGEventSource.h>).

Urinalysis answered 10/1, 2011 at 4:44 Comment(4)
Is it possible to do the same on iOS?Sipes
Neither of these APIs is available on iOS. I don't know if there are any equivalents either.Urinalysis
Do not use CGEventSourceSecondsSinceLastEventType in launch daemon.Lamelli
Same as with HIDIdleTime: this function always returns zero on some machines for unknown reasons (probably related to connected devices).Pic
C
6

In case you can't link to Carbon (ie. you want to compile x86_64 bit binary) you can wrap this function (which returns current idle time in seconds resolution as double - CFTimeInterval) in a timer:

#include <IOKit/IOKitLib.h>

CFTimeInterval CFDateGetIdleTimeInterval() {
    mach_port_t port;
    io_iterator_t iter;
    CFTypeRef value = kCFNull;
    uint64_t idle = 0;
    CFMutableDictionaryRef properties = NULL;
    io_registry_entry_t entry;

    IOMasterPort(MACH_PORT_NULL, &port);
    IOServiceGetMatchingServices(port, IOServiceMatching("IOHIDSystem"), &iter);
    if (iter) {
        if ((entry = IOIteratorNext(iter))) {
            if (IORegistryEntryCreateCFProperties(entry, &properties, kCFAllocatorDefault, 0) == KERN_SUCCESS && properties) {
                if (CFDictionaryGetValueIfPresent(properties, CFSTR("HIDIdleTime"), &value)) {
                    if (CFGetTypeID(value) == CFDataGetTypeID()) {
                        CFDataGetBytes(value, CFRangeMake(0, sizeof(idle)), (UInt8 *) &idle);
                    } else if (CFGetTypeID(value) == CFNumberGetTypeID()) {
                        CFNumberGetValue(value, kCFNumberSInt64Type, &idle);
                    }
                }
                CFRelease(properties);
            }
            IOObjectRelease(entry);
        }
        IOObjectRelease(iter);
    }

    return idle / 1000000000.0;
}

You'll need to link your code to IOKit.framework

Caron answered 6/1, 2012 at 9:34 Comment(2)
Small edit, entry = IOIteratorNext(tier) should be entry = IOIteratorNext(iter).Whitby
Please note that HIDIdleTime is not very reliable and it always returns zero on some Macs for unknown reasons (probably some connected devices reset the idle time count).Pic
R
1

Apple's Technical Q&A QA1340 Registering and unregistering for sleep and wake notifications may be what you are looking for.

If you need more control than NSWorkspaceWillSleepNotification (Listing 1), use I/O Kit and register to receive power notifications (Listing 3).

Repel answered 10/1, 2011 at 4:25 Comment(2)
The problem with depending on sleep/wake notifications is that if a user sets their sleep settings to "never" they will never be sent. I would like to count idle time as time when there is no keyboard/mouse activity.Yoruba
Sorry, though you meant when the app enters ideal sleep. kperryua's solution with CGEventSourceSecondsSinceLastEventType is correct for monitoring time since last user input.Repel
S
1

I used a different approach. Subclassing UIApplication I override the sendEvent method filtering touches (actually you can filter any kind of event, acceleration, touches, etc.). Using a shared variable and a background timer I managed the "idle". Every time the user touch the screen the variable is set with current timeInterval (current time). The timer fire method checks for the elapsed time since last touch, if greater than the threshold (in my case was around 90seconds) you can POST your own notification.

I used this simple approach to create a custom set of apps that after some idle time automatically call the "screensaver" app.

Nothing clever, it just do the job.

Hope that helps.

Sipes answered 22/2, 2012 at 21:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.