blob: f6bc848f720ffdafe8a171ec74da741a5857052a [file] [log] [blame]
/*****************************************************************************
* HIDRemoteControlDevice.m
* RemoteControlWrapper
*
* Created by Martin Kahr on 11.03.06 under a MIT-style license.
* Copyright (c) 2006 martinkahr.com. All rights reserved.
*
* Code modified and adapted to OpenOffice.org
* by Eric Bachard on 11.08.2008 under the same license
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*****************************************************************************/
#import "HIDRemoteControlDevice.h"
#import <mach/mach.h>
#import <mach/mach_error.h>
#import <IOKit/IOKitLib.h>
#import <IOKit/IOCFPlugIn.h>
#import <IOKit/hid/IOHIDKeys.h>
#import <Carbon/Carbon.h>
@interface HIDRemoteControlDevice (PrivateMethods)
- (NSDictionary*) cookieToButtonMapping; // Creates the dictionary using the magics, depending on the remote
- (IOHIDQueueInterface**) queue;
- (IOHIDDeviceInterface**) hidDeviceInterface;
- (void) handleEventWithCookieString: (NSString*) cookieString sumOfValues: (SInt32) sumOfValues;
- (void) removeNotifcationObserver;
- (void) remoteControlAvailable:(NSNotification *)notification;
@end
@interface HIDRemoteControlDevice (IOKitMethods)
+ (io_object_t) findRemoteDevice;
- (IOHIDDeviceInterface**) createInterfaceForDevice: (io_object_t) hidDevice;
- (BOOL) initializeCookies;
- (BOOL) openDevice;
@end
@implementation HIDRemoteControlDevice
+ (const char*) remoteControlDeviceName {
return "";
}
+ (BOOL) isRemoteAvailable {
io_object_t hidDevice = [self findRemoteDevice];
if (hidDevice != 0) {
IOObjectRelease(hidDevice);
return YES;
} else {
return NO;
}
}
- (id) initWithDelegate: (id) _remoteControlDelegate {
if ([[self class] isRemoteAvailable] == NO) return nil;
if ( (self = [super initWithDelegate: _remoteControlDelegate]) ) {
openInExclusiveMode = YES;
queue = NULL;
hidDeviceInterface = NULL;
cookieToButtonMapping = [[NSMutableDictionary alloc] init];
[self setCookieMappingInDictionary: cookieToButtonMapping];
NSEnumerator* enumerator = [cookieToButtonMapping objectEnumerator];
NSNumber* identifier;
supportedButtonEvents = 0;
while( (identifier = [enumerator nextObject]) ) {
supportedButtonEvents |= [identifier intValue];
}
fixSecureEventInputBug = [[NSUserDefaults standardUserDefaults] boolForKey: @"remoteControlWrapperFixSecureEventInputBug"];
}
return self;
}
- (void) dealloc {
[self removeNotifcationObserver];
[self stopListening:self];
[cookieToButtonMapping release];
[super dealloc];
}
- (void) sendRemoteButtonEvent: (RemoteControlEventIdentifier) event pressedDown: (BOOL) pressedDown {
[delegate sendRemoteButtonEvent: event pressedDown: pressedDown remoteControl:self];
}
- (void) setCookieMappingInDictionary: (NSMutableDictionary*) cookieToButtonMapping {
}
- (int) remoteIdSwitchCookie {
return 0;
}
- (BOOL) sendsEventForButtonIdentifier: (RemoteControlEventIdentifier) identifier {
return (supportedButtonEvents & identifier) == identifier;
}
- (BOOL) isListeningToRemote {
return (hidDeviceInterface != NULL && allCookies != NULL && queue != NULL);
}
- (void) setListeningToRemote: (BOOL) value {
if (value == NO) {
[self stopListening:self];
} else {
[self startListening:self];
}
}
- (BOOL) isOpenInExclusiveMode {
return openInExclusiveMode;
}
- (void) setOpenInExclusiveMode: (BOOL) value {
openInExclusiveMode = value;
}
- (BOOL) processesBacklog {
return processesBacklog;
}
- (void) setProcessesBacklog: (BOOL) value {
processesBacklog = value;
}
- (void) startListening: (id) sender {
if ([self isListeningToRemote]) return;
// 4th July 2007
//
// A security update in february of 2007 introduced an odd behavior.
// Whenever SecureEventInput is activated or deactivated the exclusive access
// to the remote control device is lost. This leads to very strange behavior where
// a press on the Menu button activates FrontRow while your app still gets the event.
// A great number of people have complained about this.
//
// Enabling the SecureEventInput and keeping it enabled does the trick.
//
// I'm pretty sure this is a kind of bug at Apple and I'm in contact with the responsible
// Apple Engineer. This solution is not a perfect one - I know.
// One of the side effects is that applications that listen for special global keyboard shortcuts (like Quicksilver)
// may get into problems as they no longer get the events.
// As there is no official Apple Remote API from Apple I also failed to open a technical incident on this.
//
// Note that there is a corresponding DisableSecureEventInput in the stopListening method below.
//
if ([self isOpenInExclusiveMode] && fixSecureEventInputBug) EnableSecureEventInput();
[self removeNotifcationObserver];
io_object_t hidDevice = [[self class] findRemoteDevice];
if (hidDevice == 0) return;
if ([self createInterfaceForDevice:hidDevice] == NULL) {
goto error;
}
if ([self initializeCookies]==NO) {
goto error;
}
if ([self openDevice]==NO) {
goto error;
}
// be KVO friendly
[self willChangeValueForKey:@"listeningToRemote"];
[self didChangeValueForKey:@"listeningToRemote"];
goto cleanup;
error:
[self stopListening:self];
DisableSecureEventInput();
cleanup:
IOObjectRelease(hidDevice);
}
- (void) stopListening: (id) sender {
if ([self isListeningToRemote]==NO) return;
BOOL sendNotification = NO;
if (eventSource != NULL) {
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), eventSource, kCFRunLoopDefaultMode);
CFRelease(eventSource);
eventSource = NULL;
}
if (queue != NULL) {
(*queue)->stop(queue);
//dispose of queue
(*queue)->dispose(queue);
//release the queue we allocated
(*queue)->Release(queue);
queue = NULL;
sendNotification = YES;
}
if (allCookies != nil) {
[allCookies autorelease];
allCookies = nil;
}
if (hidDeviceInterface != NULL) {
//close the device
(*hidDeviceInterface)->close(hidDeviceInterface);
//release the interface
(*hidDeviceInterface)->Release(hidDeviceInterface);
hidDeviceInterface = NULL;
}
if ([self isOpenInExclusiveMode] && fixSecureEventInputBug) DisableSecureEventInput();
if ([self isOpenInExclusiveMode] && sendNotification) {
[[self class] sendFinishedNotifcationForAppIdentifier: nil];
}
// be KVO friendly
[self willChangeValueForKey:@"listeningToRemote"];
[self didChangeValueForKey:@"listeningToRemote"];
}
@end
@implementation HIDRemoteControlDevice (PrivateMethods)
- (IOHIDQueueInterface**) queue {
return queue;
}
- (IOHIDDeviceInterface**) hidDeviceInterface {
return hidDeviceInterface;
}
- (NSDictionary*) cookieToButtonMapping {
return cookieToButtonMapping;
}
- (NSString*) validCookieSubstring: (NSString*) cookieString {
if (cookieString == nil || [cookieString length] == 0) return nil;
NSEnumerator* keyEnum = [[self cookieToButtonMapping] keyEnumerator];
NSString* key;
while( (key = [keyEnum nextObject]) ) {
NSRange range = [cookieString rangeOfString:key];
if (range.location == 0) return key;
}
return nil;
}
- (void) handleEventWithCookieString: (NSString*) cookieString sumOfValues: (SInt32) sumOfValues {
/*
if (previousRemainingCookieString) {
cookieString = [previousRemainingCookieString stringByAppendingString: cookieString];
NSLog( @"Apple Remote: New cookie string is %@", cookieString);
[previousRemainingCookieString release], previousRemainingCookieString=nil;
}*/
if (cookieString == nil || [cookieString length] == 0) return;
NSNumber* buttonId = [[self cookieToButtonMapping] objectForKey: cookieString];
if (buttonId != nil) {
switch ( (int)buttonId )
{
case kMetallicRemote2009ButtonPlay:
case kMetallicRemote2009ButtonMiddlePlay:
buttonId = [NSNumber numberWithInt:kRemoteButtonPlay];
break;
default:
break;
}
[self sendRemoteButtonEvent: [buttonId intValue] pressedDown: (sumOfValues>0)];
} else {
// let's see if a number of events are stored in the cookie string. this does
// happen when the main thread is too busy to handle all incoming events in time.
NSString* subCookieString;
NSString* lastSubCookieString=nil;
while( (subCookieString = [self validCookieSubstring: cookieString]) ) {
cookieString = [cookieString substringFromIndex: [subCookieString length]];
lastSubCookieString = subCookieString;
if (processesBacklog) [self handleEventWithCookieString: subCookieString sumOfValues:sumOfValues];
}
if (processesBacklog == NO && lastSubCookieString != nil) {
// process the last event of the backlog and assume that the button is not pressed down any longer.
// The events in the backlog do not seem to be in order and therefore (in rare cases) the last event might be
// a button pressed down event while in reality the user has released it.
// NSLog(@"processing last event of backlog");
[self handleEventWithCookieString: lastSubCookieString sumOfValues:0];
}
if ([cookieString length] > 0) {
NSLog( @"Apple Remote: Unknown button for cookiestring %@", cookieString);
}
}
}
- (void) removeNotifcationObserver {
[[NSDistributedNotificationCenter defaultCenter] removeObserver:self name:FINISHED_USING_REMOTE_CONTROL_NOTIFICATION object:nil];
}
- (void) remoteControlAvailable:(NSNotification *)notification {
[self removeNotifcationObserver];
[self startListening: self];
}
@end
/* Callback method for the device queue
Will be called for any event of any type (cookie) to which we subscribe
*/
static void QueueCallbackFunction(void* target, IOReturn result, void* refcon, void* sender) {
if (target < 0) {
NSLog( @"Apple Remote: QueueCallbackFunction called with invalid target!");
return;
}
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
HIDRemoteControlDevice* remote = (HIDRemoteControlDevice*)target;
IOHIDEventStruct event;
AbsoluteTime zeroTime = {0,0};
NSMutableString* cookieString = [NSMutableString string];
SInt32 sumOfValues = 0;
while (result == kIOReturnSuccess)
{
result = (*[remote queue])->getNextEvent([remote queue], &event, zeroTime, 0);
if ( result != kIOReturnSuccess )
continue;
//printf("%d %d %d\n", event.elementCookie, event.value, event.longValue);
if (((int)event.elementCookie)!=5) {
sumOfValues+=event.value;
[cookieString appendString:[NSString stringWithFormat:@"%d_", event.elementCookie]];
}
}
[remote handleEventWithCookieString: cookieString sumOfValues: sumOfValues];
[pool release];
}
@implementation HIDRemoteControlDevice (IOKitMethods)
- (IOHIDDeviceInterface**) createInterfaceForDevice: (io_object_t) hidDevice {
io_name_t className;
IOCFPlugInInterface** plugInInterface = NULL;
HRESULT plugInResult = S_OK;
SInt32 score = 0;
IOReturn ioReturnValue = kIOReturnSuccess;
hidDeviceInterface = NULL;
ioReturnValue = IOObjectGetClass(hidDevice, className);
if (ioReturnValue != kIOReturnSuccess) {
NSLog( @"Apple Remote: Error: Failed to get RemoteControlDevice class name.");
return NULL;
}
ioReturnValue = IOCreatePlugInInterfaceForService(hidDevice,
kIOHIDDeviceUserClientTypeID,
kIOCFPlugInInterfaceID,
&plugInInterface,
&score);
if (ioReturnValue == kIOReturnSuccess)
{
//Call a method of the intermediate plug-in to create the device interface
plugInResult = (*plugInInterface)->QueryInterface(plugInInterface, CFUUIDGetUUIDBytes(kIOHIDDeviceInterfaceID), (LPVOID) &hidDeviceInterface);
if (plugInResult != S_OK) {
NSLog( @"Apple Remote: Error: Couldn't create HID class device interface");
}
// Release
if (plugInInterface) (*plugInInterface)->Release(plugInInterface);
}
return hidDeviceInterface;
}
- (BOOL) initializeCookies {
IOHIDDeviceInterface122** handle = (IOHIDDeviceInterface122**)hidDeviceInterface;
IOHIDElementCookie cookie;
long usage;
long usagePage;
id object;
NSArray* elements = nil;
NSDictionary* element;
IOReturn success;
if (!handle || !(*handle)) return NO;
// Copy all elements, since we're grabbing most of the elements
// for this device anyway, and thus, it's faster to iterate them
// ourselves. When grabbing only one or two elements, a matching
// dictionary should be passed in here instead of NULL.
success = (*handle)->copyMatchingElements(handle, NULL, (CFArrayRef*)&elements);
if (success == kIOReturnSuccess) {
[elements autorelease];
/*
cookies = calloc(NUMBER_OF_APPLE_REMOTE_ACTIONS, sizeof(IOHIDElementCookie));
memset(cookies, 0, sizeof(IOHIDElementCookie) * NUMBER_OF_APPLE_REMOTE_ACTIONS);
*/
allCookies = [[NSMutableArray alloc] init];
NSEnumerator *elementsEnumerator = [elements objectEnumerator];
while ( (element = [elementsEnumerator nextObject]) ) {
//Get cookie
object = [element valueForKey: (NSString*)CFSTR(kIOHIDElementCookieKey) ];
if (object == nil || ![object isKindOfClass:[NSNumber class]]) continue;
if (object == 0 || CFGetTypeID(object) != CFNumberGetTypeID()) continue;
cookie = (IOHIDElementCookie) [object longValue];
//Get usage
object = [element valueForKey: (NSString*)CFSTR(kIOHIDElementUsageKey) ];
if (object == nil || ![object isKindOfClass:[NSNumber class]]) continue;
usage = [object longValue];
//Get usage page
object = [element valueForKey: (NSString*)CFSTR(kIOHIDElementUsagePageKey) ];
if (object == nil || ![object isKindOfClass:[NSNumber class]]) continue;
usagePage = [object longValue];
[allCookies addObject: [NSNumber numberWithInt:(int)cookie]];
}
} else {
return NO;
}
return YES;
}
- (BOOL) openDevice {
HRESULT result;
IOHIDOptionsType openMode = kIOHIDOptionsTypeNone;
if ([self isOpenInExclusiveMode]) openMode = kIOHIDOptionsTypeSeizeDevice;
IOReturn ioReturnValue = (*hidDeviceInterface)->open(hidDeviceInterface, openMode);
if (ioReturnValue == KERN_SUCCESS) {
queue = (*hidDeviceInterface)->allocQueue(hidDeviceInterface);
if (queue) {
result = (*queue)->create(queue, 0, 12); //depth: maximum number of elements in queue before oldest elements in queue begin to be lost.
IOHIDElementCookie cookie;
NSEnumerator *allCookiesEnumerator = [allCookies objectEnumerator];
while ( (cookie = (IOHIDElementCookie)[[allCookiesEnumerator nextObject] intValue]) ) {
(*queue)->addElement(queue, cookie, 0);
}
// add callback for async events
ioReturnValue = (*queue)->createAsyncEventSource(queue, &eventSource);
if (ioReturnValue == KERN_SUCCESS) {
ioReturnValue = (*queue)->setEventCallout(queue,QueueCallbackFunction, self, NULL);
if (ioReturnValue == KERN_SUCCESS) {
CFRunLoopAddSource(CFRunLoopGetCurrent(), eventSource, kCFRunLoopDefaultMode);
//start data delivery to queue
(*queue)->start(queue);
return YES;
} else {
NSLog( @"Apple Remote: Error when setting event callback");
}
} else {
NSLog( @"Apple Remote: Error when creating async event source");
}
} else {
NSLog( @"Apple Remote: Error when opening device");
}
} else if (ioReturnValue == kIOReturnExclusiveAccess) {
// the device is used exclusive by another application
// 1. we register for the FINISHED_USING_REMOTE_CONTROL_NOTIFICATION notification
[[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(remoteControlAvailable:) name:FINISHED_USING_REMOTE_CONTROL_NOTIFICATION object:nil];
// 2. send a distributed notification that we wanted to use the remote control
[[self class] sendRequestForRemoteControlNotification];
}
return NO;
}
+ (io_object_t) findRemoteDevice {
CFMutableDictionaryRef hidMatchDictionary = NULL;
IOReturn ioReturnValue = kIOReturnSuccess;
io_iterator_t hidObjectIterator = 0;
io_object_t hidDevice = 0;
// Set up a matching dictionary to search the I/O Registry by class
// name for all HID class devices
hidMatchDictionary = IOServiceMatching([self remoteControlDeviceName]);
// Now search I/O Registry for matching devices.
ioReturnValue = IOServiceGetMatchingServices(kIOMasterPortDefault, hidMatchDictionary, &hidObjectIterator);
if ((ioReturnValue == kIOReturnSuccess) && (hidObjectIterator != 0)) {
hidDevice = IOIteratorNext(hidObjectIterator);
}
// release the iterator
IOObjectRelease(hidObjectIterator);
return hidDevice;
}
@end