diff --git a/Utils/AppleRemote.h b/Utils/AppleRemote.h new file mode 100644 index 000000000..8ca112ae8 --- /dev/null +++ b/Utils/AppleRemote.h @@ -0,0 +1,199 @@ +/***************************************************************************** + * AppleRemote.h + * AppleRemote + * $Id: AppleRemote.h 18683 2007-02-02 09:12:37Z fkuehne $ + * + * Created by Martin Kahr on 11.03.06 under a MIT-style license. + * Copyright (c) 2006 martinkahr.com. All rights reserved. + * + * 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. + * + ***************************************************************************** + * + * Note that changes made by any members or contributors of the VideoLAN team + * (i.e. changes that were checked in exclusively into one of VideoLAN's source code + * repositories) are licensed under the GNU General Public License version 2, + * or (at your option) any later version. + * Thus, the following statements apply to our changes: + * + * Copyright (C) 2006-2007 the VideoLAN team + * Authors: Eric Petit + * Felix Kühne + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. + *****************************************************************************/ + +#import +#import +#import +#import +#import +#import +#import + +enum AppleRemoteEventIdentifier +{ + kRemoteButtonVolume_Plus =1<<1, + kRemoteButtonVolume_Minus =1<<2, + kRemoteButtonMenu =1<<3, + kRemoteButtonPlay =1<<4, + kRemoteButtonRight =1<<5, + kRemoteButtonLeft =1<<6, + kRemoteButtonRight_Hold =1<<7, + kRemoteButtonLeft_Hold =1<<8, + kRemoteButtonMenu_Hold =1<<9, + kRemoteButtonPlay_Sleep =1<<10, + kRemoteControl_Switched =1<<11, + kRemoteButtonVolume_Plus_Hold =1<<12, + kRemoteButtonVolume_Minus_Hold =1<<13 +}; +typedef enum AppleRemoteEventIdentifier AppleRemoteEventIdentifier; + +/* Encapsulates usage of the apple remote control +This class is implemented as a singleton as there is exactly one remote per machine (until now) +The class is not thread safe +*/ +@interface AppleRemote : NSObject { + IOHIDDeviceInterface** hidDeviceInterface; + IOHIDQueueInterface** queue; + NSMutableArray* allCookies; + NSMutableDictionary* cookieToButtonMapping; + + BOOL openInExclusiveMode; + BOOL simulatePlusMinusHold; + BOOL processesBacklog; + + /* state for simulating plus/minus hold */ + BOOL lastEventSimulatedHold; + AppleRemoteEventIdentifier lastPlusMinusEvent; + NSTimeInterval lastPlusMinusEventTime; + + int remoteId; + unsigned int clickCountEnabledButtons; + NSTimeInterval maxClickTimeDifference; + NSTimeInterval lastClickCountEventTime; + AppleRemoteEventIdentifier lastClickCountEvent; + unsigned int eventClickCount; + + IBOutlet id delegate; +} + +- (int) remoteId; + +- (BOOL) isRemoteAvailable; + +- (BOOL) isListeningToRemote; +- (void) setListeningToRemote: (BOOL) value; + +- (BOOL) isOpenInExclusiveMode; +- (void) setOpenInExclusiveMode: (BOOL) value; + +/* click counting makes it possible to recognize if the user has pressed a button repeatedly + * click counting does delay each event as it has to wait if there is another event (second click) + * therefore there is a slight time difference (maximumClickCountTimeDifference) between a single click + * of the user and the call of your delegate method + * click counting can be enabled individually for specific buttons. Use the property clickCountEnableButtons + * to set the buttons for which click counting shall be enabled */ +- (BOOL) clickCountingEnabled; +- (void) setClickCountingEnabled: (BOOL) value; + +- (unsigned int) clickCountEnabledButtons; +- (void) setClickCountEnabledButtons: (unsigned int)value; + +/* the maximum time difference till which clicks are recognized as multi clicks */ +- (NSTimeInterval) maximumClickCountTimeDifference; +- (void) setMaximumClickCountTimeDifference: (NSTimeInterval) timeDiff; + +/* When your application needs to much time on the main thread when processing an event other events + * may already be received which are put on a backlog. As soon as your main thread + * has some spare time this backlog is processed and may flood your delegate with calls. + * Backlog processing is turned off by default. */ +- (BOOL) processesBacklog; +- (void) setProcessesBacklog: (BOOL) value; + +/* Sets an NSApplication delegate which starts listening when application is becoming active + * and stops listening when application resigns being active. + * If an NSApplication delegate has been already set all method calls will be forwarded to this delegate, too. */ +- (BOOL) listeningOnAppActivate; +- (void) setListeningOnAppActivate: (BOOL) value; + +/* Simulating plus/minus hold does deactivate sending of individual requests for plus/minus pressed down/released. + * Instead special hold events are being triggered when the user is pressing and holding plus/minus for a small period. + * With simulating enabled the plus/minus buttons do behave as the left/right buttons */ +- (BOOL) simulatesPlusMinusHold; +- (void) setSimulatesPlusMinusHold: (BOOL) value; + +/* Delegates are not retained */ +- (void) setDelegate: (id) delegate; +- (id) delegate; + +- (IBAction) startListening: (id) sender; +- (IBAction) stopListening: (id) sender; +@end + +@interface AppleRemote (Singleton) + ++ (AppleRemote*) sharedRemote; + +@end + +/* Method definitions for the delegate of the AppleRemote class */ +@interface NSObject(NSAppleRemoteDelegate) + +- (void) appleRemoteButton: (AppleRemoteEventIdentifier)buttonIdentifier pressedDown: (BOOL) pressedDown clickCount: (unsigned int) count; + +@end + +@interface AppleRemote (PrivateMethods) +- (void) setRemoteId: (int) aValue; +- (NSDictionary*) cookieToButtonMapping; +- (IOHIDQueueInterface**) queue; +- (IOHIDDeviceInterface**) hidDeviceInterface; +- (void) handleEventWithCookieString: (NSString*) cookieString sumOfValues: (SInt32) sumOfValues; +@end + +@interface AppleRemote (IOKitMethods) +- (io_object_t) findAppleRemoteDevice; +- (IOHIDDeviceInterface**) createInterfaceForDevice: (io_object_t) hidDevice; +- (BOOL) initializeCookies; +- (BOOL) openDevice; +@end + +/* A NSApplication delegate which is used to activate and deactivate listening to the remote control + * dependent on the activation state of your application. + * All events are delegated to the original NSApplication delegate if necessary */ +@interface AppleRemoteApplicationDelegate : NSObject { + id applicationDelegate; +} + +- (id) initWithApplicationDelegate: (id) delegate; +- (id) applicationDelegate; +@end \ No newline at end of file diff --git a/Utils/AppleRemote.m b/Utils/AppleRemote.m new file mode 100644 index 000000000..fbe986bdc --- /dev/null +++ b/Utils/AppleRemote.m @@ -0,0 +1,713 @@ +/***************************************************************************** + * AppleRemote.m + * AppleRemote + * $Id: AppleRemote.m 18683 2007-02-02 09:12:37Z fkuehne $ + * + * Created by Martin Kahr on 11.03.06 under a MIT-style license. + * Copyright (c) 2006 martinkahr.com. All rights reserved. + * + * 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. + * + ***************************************************************************** + * + * Note that changes made by any members or contributors of the VideoLAN team + * (i.e. changes that were exclusively checked in to one of VideoLAN's source code + * repositories) are licensed under the GNU General Public License version 2, + * or (at your option) any later version. + * Thus, the following statements apply to our changes: + * + * Copyright (C) 2006-2007 the VideoLAN team + * Authors: Eric Petit + * Felix Kühne + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. + *****************************************************************************/ + +#import "AppleRemote.h" + +const char* AppleRemoteDeviceName = "AppleIRController"; +const int REMOTE_SWITCH_COOKIE=19; +const NSTimeInterval DEFAULT_MAXIMUM_CLICK_TIME_DIFFERENCE=0.35; +const NSTimeInterval HOLD_RECOGNITION_TIME_INTERVAL=0.4; + +@implementation AppleRemote + +#pragma public interface + +- (id) init { + if ( self = [super init] ) { + openInExclusiveMode = YES; + queue = NULL; + hidDeviceInterface = NULL; + cookieToButtonMapping = [[NSMutableDictionary alloc] init]; + + [cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonVolume_Plus] forKey:@"14_12_11_6_"]; + [cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonVolume_Minus] forKey:@"14_13_11_6_"]; + [cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonMenu] forKey:@"14_7_6_14_7_6_"]; + [cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonPlay] forKey:@"14_8_6_14_8_6_"]; + [cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonRight] forKey:@"14_9_6_14_9_6_"]; + [cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonLeft] forKey:@"14_10_6_14_10_6_"]; + [cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonRight_Hold] forKey:@"14_6_4_2_"]; + [cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonLeft_Hold] forKey:@"14_6_3_2_"]; + [cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonMenu_Hold] forKey:@"14_6_14_6_"]; + [cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteButtonPlay_Sleep] forKey:@"18_14_6_18_14_6_"]; + [cookieToButtonMapping setObject:[NSNumber numberWithInt:kRemoteControl_Switched] forKey:@"19_"]; + + /* defaults */ + [self setSimulatesPlusMinusHold: YES]; + maxClickTimeDifference = DEFAULT_MAXIMUM_CLICK_TIME_DIFFERENCE; + } + + return self; +} + +- (void) dealloc { + [self stopListening:self]; + [cookieToButtonMapping release]; + [super dealloc]; +} + +/* this was added by the VideoLAN team to ensure Leopard-compatibility and is VLC-only */ +#if GC_ENABLED +- (void)finalize +{ + [self stopListening: self]; + [super finalize]; +} +#endif + +- (int) remoteId { + return remoteId; +} + +- (BOOL) isRemoteAvailable { + io_object_t hidDevice = [self findAppleRemoteDevice]; + if (hidDevice != 0) { + IOObjectRelease(hidDevice); + return YES; + } else { + return NO; + } +} + +- (BOOL) isListeningToRemote { + return (hidDeviceInterface != NULL && allCookies != NULL && queue != NULL); +} + +- (void) setListeningToRemote: (BOOL) value { + if (value == NO) { + [self stopListening:self]; + } else { + [self startListening:self]; + } +} + +/* Delegates are not retained! + * http://developer.apple.com/documentation/Cocoa/Conceptual/CocoaFundamentals/CommunicatingWithObjects/chapter_6_section_4.html + * Delegating objects do not (and should not) retain their delegates. + * However, clients of delegating objects (applications, usually) are responsible for ensuring that their delegates are around + * to receive delegation messages. To do this, they may have to retain the delegate. */ +- (void) setDelegate: (id) _delegate { + if (_delegate && [_delegate respondsToSelector:@selector(appleRemoteButton:pressedDown:clickCount:)]==NO) return; + + delegate = _delegate; +} +- (id) delegate { + return delegate; +} + +- (BOOL) isOpenInExclusiveMode { + return openInExclusiveMode; +} +- (void) setOpenInExclusiveMode: (BOOL) value { + openInExclusiveMode = value; +} + +- (BOOL) clickCountingEnabled { + return clickCountEnabledButtons != 0; +} +- (void) setClickCountingEnabled: (BOOL) value { + if (value) { + [self setClickCountEnabledButtons: kRemoteButtonVolume_Plus | kRemoteButtonVolume_Minus | kRemoteButtonPlay | kRemoteButtonLeft | kRemoteButtonRight | kRemoteButtonMenu]; + } else { + [self setClickCountEnabledButtons: 0]; + } +} + +- (unsigned int) clickCountEnabledButtons { + return clickCountEnabledButtons; +} +- (void) setClickCountEnabledButtons: (unsigned int)value { + clickCountEnabledButtons = value; +} + +- (NSTimeInterval) maximumClickCountTimeDifference { + return maxClickTimeDifference; +} +- (void) setMaximumClickCountTimeDifference: (NSTimeInterval) timeDiff { + maxClickTimeDifference = timeDiff; +} + +- (BOOL) processesBacklog { + return processesBacklog; +} +- (void) setProcessesBacklog: (BOOL) value { + processesBacklog = value; +} + +- (BOOL) listeningOnAppActivate { + id appDelegate = [NSApp delegate]; + return (appDelegate!=nil && [appDelegate isKindOfClass: [AppleRemoteApplicationDelegate class]]); +} +- (void) setListeningOnAppActivate: (BOOL) value { + if (value) { + if ([self listeningOnAppActivate]) return; + AppleRemoteApplicationDelegate* appDelegate = [[AppleRemoteApplicationDelegate alloc] initWithApplicationDelegate: [NSApp delegate]]; + /* NSApp does not retain its delegate therefore we keep retain count on 1 */ + [NSApp setDelegate: appDelegate]; + } else { + if ([self listeningOnAppActivate]==NO) return; + AppleRemoteApplicationDelegate* appDelegate = (AppleRemoteApplicationDelegate*)[NSApp delegate]; + id previousAppDelegate = [appDelegate applicationDelegate]; + [NSApp setDelegate: previousAppDelegate]; + [appDelegate release]; + } +} + +- (BOOL) simulatesPlusMinusHold { + return simulatePlusMinusHold; +} +- (void) setSimulatesPlusMinusHold: (BOOL) value { + simulatePlusMinusHold = value; +} + +- (IBAction) startListening: (id) sender { + if ([self isListeningToRemote]) return; + + io_object_t hidDevice = [self findAppleRemoteDevice]; + if (hidDevice == 0) return; + + if ([self createInterfaceForDevice:hidDevice] == NULL) { + goto error; + } + + if ([self initializeCookies]==NO) { + goto error; + } + + if ([self openDevice]==NO) { + goto error; + } + goto cleanup; + +error: + [self stopListening:self]; + +cleanup: + IOObjectRelease(hidDevice); +} + +- (IBAction) stopListening: (id) sender { + if (queue != NULL) { + (*queue)->stop(queue); + + //dispose of queue + (*queue)->dispose(queue); + + //release the queue we allocated + (*queue)->Release(queue); + + queue = NULL; + } + + if (allCookies != nil) { + [allCookies autorelease]; + allCookies = nil; + } + + if (hidDeviceInterface != NULL) { + //close the device + (*hidDeviceInterface)->close(hidDeviceInterface); + + //release the interface + (*hidDeviceInterface)->Release(hidDeviceInterface); + + hidDeviceInterface = NULL; + } +} + +@end + +@implementation AppleRemote (Singleton) + +static AppleRemote* sharedInstance=nil; + ++ (AppleRemote*) sharedRemote { + @synchronized(self) { + if (sharedInstance == nil) { + sharedInstance = [[self alloc] init]; + } + } + return sharedInstance; +} ++ (id)allocWithZone:(NSZone *)zone { + @synchronized(self) { + if (sharedInstance == nil) { + return [super allocWithZone:zone]; + } + } + return sharedInstance; +} +- (id)copyWithZone:(NSZone *)zone { + return self; +} +- (id)retain { + return self; +} +- (unsigned)retainCount { + return UINT_MAX; //denotes an object that cannot be released +} +- (void)release { + //do nothing +} +- (id)autorelease { + return self; +} + +@end + +@implementation AppleRemote (PrivateMethods) + +- (void) setRemoteId: (int) value { + remoteId = value; +} + +- (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) sendSimulatedPlusMinusEvent: (id) time { + BOOL startSimulateHold = NO; + AppleRemoteEventIdentifier event = lastPlusMinusEvent; + @synchronized(self) { + startSimulateHold = (lastPlusMinusEvent>0 && lastPlusMinusEventTime == [time doubleValue]); + } + if (startSimulateHold) { + lastEventSimulatedHold = YES; + event = (event==kRemoteButtonVolume_Plus) ? kRemoteButtonVolume_Plus_Hold : kRemoteButtonVolume_Minus_Hold; + [delegate appleRemoteButton:event pressedDown: YES clickCount: 1]; + } +} + +- (void) sendRemoteButtonEvent: (AppleRemoteEventIdentifier) event pressedDown: (BOOL) pressedDown { + if (delegate) { + if (simulatePlusMinusHold) { + if (event == kRemoteButtonVolume_Plus || event == kRemoteButtonVolume_Minus) { + if (pressedDown) { + lastPlusMinusEvent = event; + lastPlusMinusEventTime = [NSDate timeIntervalSinceReferenceDate]; + [self performSelector:@selector(sendSimulatedPlusMinusEvent:) + withObject:[NSNumber numberWithDouble:lastPlusMinusEventTime] + afterDelay:HOLD_RECOGNITION_TIME_INTERVAL]; + return; + } else { + if (lastEventSimulatedHold) { + event = (event==kRemoteButtonVolume_Plus) ? kRemoteButtonVolume_Plus_Hold : kRemoteButtonVolume_Minus_Hold; + lastPlusMinusEvent = 0; + lastEventSimulatedHold = NO; + } else { + @synchronized(self) { + lastPlusMinusEvent = 0; + } + pressedDown = YES; + } + } + } + } + + if (([self clickCountEnabledButtons] & event) == event) { + if (pressedDown==NO && (event == kRemoteButtonVolume_Minus || event == kRemoteButtonVolume_Plus)) { + return; // this one is triggered automatically by the handler + } + NSNumber* eventNumber; + NSNumber* timeNumber; + @synchronized(self) { + lastClickCountEventTime = [NSDate timeIntervalSinceReferenceDate]; + if (lastClickCountEvent == event) { + eventClickCount = eventClickCount + 1; + } else { + eventClickCount = 1; + } + lastClickCountEvent = event; + timeNumber = [NSNumber numberWithDouble:lastClickCountEventTime]; + eventNumber= [NSNumber numberWithUnsignedInt:event]; + } + [self performSelector: @selector(executeClickCountEvent:) + withObject: [NSArray arrayWithObjects:eventNumber, timeNumber, nil] + afterDelay: maxClickTimeDifference]; + } else { + [delegate appleRemoteButton:event pressedDown: pressedDown clickCount:1]; + } + } +} + +- (void) executeClickCountEvent: (NSArray*) values { + AppleRemoteEventIdentifier event = [[values objectAtIndex: 0] unsignedIntValue]; + NSTimeInterval eventTimePoint = [[values objectAtIndex: 1] doubleValue]; + + BOOL finishedClicking = NO; + int finalClickCount = eventClickCount; + + @synchronized(self) { + finishedClicking = (event != lastClickCountEvent || eventTimePoint == lastClickCountEventTime); + if (finishedClicking) eventClickCount = 0; + } + + if (finishedClicking) { + [delegate appleRemoteButton:event pressedDown: YES clickCount:finalClickCount]; + if ([self simulatesPlusMinusHold]==NO && (event == kRemoteButtonVolume_Minus || event == kRemoteButtonVolume_Plus)) { + // trigger a button release event, too + [NSThread sleepUntilDate: [NSDate dateWithTimeIntervalSinceNow:0.1]]; + [delegate appleRemoteButton:event pressedDown: NO clickCount:finalClickCount]; + } + } + +} + +- (void) handleEventWithCookieString: (NSString*) cookieString sumOfValues: (SInt32) sumOfValues { + /* + if (previousRemainingCookieString) { + cookieString = [previousRemainingCookieString stringByAppendingString: cookieString]; + NSLog(@"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) { + [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(@"Unknown button for cookiestring %@", cookieString); + } + } +} + +@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) { + AppleRemote* remote = (AppleRemote*)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 (REMOTE_SWITCH_COOKIE == (int)event.elementCookie) { + [remote setRemoteId: event.value]; + [remote handleEventWithCookieString: @"19_" sumOfValues: 0]; + } else { + if (((int)event.elementCookie)!=5) { + sumOfValues+=event.value; + [cookieString appendString:[NSString stringWithFormat:@"%d_", event.elementCookie]]; + } + } + } + + [remote handleEventWithCookieString: cookieString sumOfValues: sumOfValues]; +} + +@implementation AppleRemote (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(@"Error: Failed to get 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(@"Error: Couldn't create HID class device interface"); + } + // Release + if (plugInInterface) (*plugInInterface)->Release(plugInInterface); + } + return hidDeviceInterface; +} + +- (io_object_t) findAppleRemoteDevice { + 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(AppleRemoteDeviceName); + + // 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; +} + +- (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]; + int i; + for (i=0; i< [elements count]; i++) { + element = [elements objectAtIndex:i]; + + //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. + + int i=0; + for(i=0; i<[allCookies count]; i++) { + IOHIDElementCookie cookie = (IOHIDElementCookie)[[allCookies objectAtIndex:i] intValue]; + (*queue)->addElement(queue, cookie, 0); + } + + // add callback for async events + CFRunLoopSourceRef eventSource; + 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(@"Error when setting event callout"); + } + } else { + NSLog(@"Error when creating async event source"); + } + } else { + NSLog(@"Error when opening device"); + } + } + return NO; +} + +@end + +@implementation AppleRemoteApplicationDelegate + +- (id) initWithApplicationDelegate: (id) delegate { + if (self = [super init]) { + applicationDelegate = [delegate retain]; + } + return self; +} + +- (void) dealloc { + NSLog(@"Dealloc"); + [applicationDelegate release]; + [super dealloc]; +} + +- (id) applicationDelegate { + return applicationDelegate; +} + +- (void)applicationWillBecomeActive:(NSNotification *)aNotification { + if ([applicationDelegate respondsToSelector: @selector(applicationWillBecomeActive:)]) { + [applicationDelegate applicationWillBecomeActive: aNotification]; + } +} +- (void)applicationDidBecomeActive:(NSNotification *)aNotification { + [[AppleRemote sharedRemote] setListeningToRemote: YES]; + + if ([applicationDelegate respondsToSelector: @selector(applicationDidBecomeActive:)]) { + [applicationDelegate applicationDidBecomeActive: aNotification]; + } +} +- (void)applicationWillResignActive:(NSNotification *)aNotification { + [[AppleRemote sharedRemote] setListeningToRemote: NO]; + + if ([applicationDelegate respondsToSelector: @selector(applicationWillResignActive:)]) { + [applicationDelegate applicationWillResignActive: aNotification]; + } +} +- (void)applicationDidResignActive:(NSNotification *)aNotification { + if ([applicationDelegate respondsToSelector: @selector(applicationDidResignActive:)]) { + [applicationDelegate applicationDidResignActive: aNotification]; + } +} + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { + NSMethodSignature* signature = [super methodSignatureForSelector: aSelector]; + if (signature == nil && applicationDelegate != nil) { + signature = [applicationDelegate methodSignatureForSelector: aSelector]; + } + return signature; +} + +- (void)forwardInvocation:(NSInvocation *)invocation { + SEL aSelector = [invocation selector]; + + if (applicationDelegate==nil || [applicationDelegate respondsToSelector:aSelector]==NO) { + [super forwardInvocation: invocation]; + return; + } + + [invocation invokeWithTarget:applicationDelegate]; +} +@end \ No newline at end of file