| /***************************************************************************** |
| * 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 |
| |