/***************************************************************************** * 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 #import #import #import #import #import @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