From 72f2942dfa575a57d61fe023c845ba711ab78f55 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Mon, 3 Jun 2013 00:52:40 +0200 Subject: osx: add Apple Remote support After killing the non functional AR support in c8fd9e5 I got much complaints so this adds AR support back in (and it works). I am using the HIDRemote class by Felix Schwarz and that part of the code is under the BSD license. I slightly modified it replacing [NSApplication sharedApplication] with NSApp. The code of the class is quite complex (probably because it had to deal with all the edge cases with IOKit) but it works nicely as a black box. In a later commit I'll remove the deprecation warnings caused by HIDRemote's usage of Gestalt. Check out `etc/input.conf` for the default bindings. Apple Remote functionality is automatically compiled in when cocoa is enabled. It can be disabled at runtime with the `--no-ar` option. --- osdep/ar/HIDRemote.h | 378 +++++++++ osdep/ar/HIDRemote.m | 2068 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 2446 insertions(+) create mode 100644 osdep/ar/HIDRemote.h create mode 100644 osdep/ar/HIDRemote.m (limited to 'osdep/ar') diff --git a/osdep/ar/HIDRemote.h b/osdep/ar/HIDRemote.h new file mode 100644 index 0000000000..9dd16faa5a --- /dev/null +++ b/osdep/ar/HIDRemote.h @@ -0,0 +1,378 @@ +// +// HIDRemote.h +// HIDRemote V1.2 +// +// Created by Felix Schwarz on 06.04.07. +// Copyright 2007-2011 IOSPIRIT GmbH. All rights reserved. +// +// The latest version of this class is available at +// http://www.iospirit.com/developers/hidremote/ +// +// ** LICENSE ************************************************************************* +// +// Copyright (c) 2007-2011 IOSPIRIT GmbH (http://www.iospirit.com/) +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, this list +// of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, this +// list of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// * Neither the name of IOSPIRIT GmbH nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior +// written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT +// SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +// TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +// BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +// DAMAGE. +// +// ************************************************************************************ + + +// ************************************************************************************ +// ********************************** DOCUMENTATION *********************************** +// ************************************************************************************ +// +// - a reference is available at http://www.iospirit.com/developers/hidremote/reference/ +// - for a guide, please see http://www.iospirit.com/developers/hidremote/guide/ +// +// ************************************************************************************ + + +#import + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#pragma mark -- Enums / Codes -- + +typedef enum +{ + kHIDRemoteModeNone = 0L, + kHIDRemoteModeShared, // Share the remote with others - let's you listen to the remote control events as long as noone has an exclusive lock on it + // (RECOMMENDED ONLY FOR SPECIAL PURPOSES) + + kHIDRemoteModeExclusive, // Try to acquire an exclusive lock on the remote (NOT RECOMMENDED) + + kHIDRemoteModeExclusiveAuto // Try to acquire an exclusive lock on the remote whenever the application has focus. Temporarily release control over the + // remote when another application has focus (RECOMMENDED) +} HIDRemoteMode; + +typedef enum +{ + /* A code reserved for "no button" (needed for tracking) */ + kHIDRemoteButtonCodeNone = 0L, + + /* Standard codes - available for white plastic and aluminum remote */ + kHIDRemoteButtonCodeUp, + kHIDRemoteButtonCodeDown, + kHIDRemoteButtonCodeLeft, + kHIDRemoteButtonCodeRight, + kHIDRemoteButtonCodeCenter, + kHIDRemoteButtonCodeMenu, + + /* Extra codes - Only available for the new aluminum version of the remote */ + kHIDRemoteButtonCodePlay, + + /* Masks */ + kHIDRemoteButtonCodeCodeMask = 0xFFL, + kHIDRemoteButtonCodeHoldMask = (1L << 16L), + kHIDRemoteButtonCodeSpecialMask = (1L << 17L), + kHIDRemoteButtonCodeAluminumMask = (1L << 21L), // PRIVATE - only used internally + + /* Hold button standard codes - available for white plastic and aluminum remote */ + kHIDRemoteButtonCodeUpHold = (kHIDRemoteButtonCodeHoldMask|kHIDRemoteButtonCodeUp), + kHIDRemoteButtonCodeDownHold = (kHIDRemoteButtonCodeHoldMask|kHIDRemoteButtonCodeDown), + kHIDRemoteButtonCodeLeftHold = (kHIDRemoteButtonCodeHoldMask|kHIDRemoteButtonCodeLeft), + kHIDRemoteButtonCodeRightHold = (kHIDRemoteButtonCodeHoldMask|kHIDRemoteButtonCodeRight), + kHIDRemoteButtonCodeCenterHold = (kHIDRemoteButtonCodeHoldMask|kHIDRemoteButtonCodeCenter), + kHIDRemoteButtonCodeMenuHold = (kHIDRemoteButtonCodeHoldMask|kHIDRemoteButtonCodeMenu), + + /* Hold button extra codes - Only available for aluminum version of the remote */ + kHIDRemoteButtonCodePlayHold = (kHIDRemoteButtonCodeHoldMask|kHIDRemoteButtonCodePlay), + + /* DEPRECATED codes - compatibility with HIDRemote 1.0 */ + kHIDRemoteButtonCodePlus = kHIDRemoteButtonCodeUp, + kHIDRemoteButtonCodePlusHold = kHIDRemoteButtonCodeUpHold, + kHIDRemoteButtonCodeMinus = kHIDRemoteButtonCodeDown, + kHIDRemoteButtonCodeMinusHold = kHIDRemoteButtonCodeDownHold, + kHIDRemoteButtonCodePlayPause = kHIDRemoteButtonCodeCenter, + kHIDRemoteButtonCodePlayPauseHold = kHIDRemoteButtonCodeCenterHold, + + /* Special purpose codes */ + kHIDRemoteButtonCodeIDChanged = (kHIDRemoteButtonCodeSpecialMask|(1L << 18L)), // (the ID of the connected remote has changed, you can safely ignore this) + #ifdef _HIDREMOTE_EXTENSIONS + #define _HIDREMOTE_EXTENSIONS_SECTION 1 + #include "HIDRemoteAdditions.h" + #undef _HIDREMOTE_EXTENSIONS_SECTION + #endif /* _HIDREMOTE_EXTENSIONS */ +} HIDRemoteButtonCode; + +typedef enum +{ + kHIDRemoteModelUndetermined = 0L, // Assume a white plastic remote + kHIDRemoteModelWhitePlastic, // Signal *likely* to be coming from a white plastic remote + kHIDRemoteModelAluminum // Signal *definitely* coming from an aluminum remote +} HIDRemoteModel; + +typedef enum +{ + kHIDRemoteAluminumRemoteSupportLevelNone = 0L, // This system has no support for the Aluminum Remote at all + kHIDRemoteAluminumRemoteSupportLevelEmulation, // This system possibly has support for the Aluminum Remote (via emulation) + kHIDRemoteAluminumRemoteSupportLevelNative // This system has native support for the Aluminum Remote +} HIDRemoteAluminumRemoteSupportLevel; + +@class HIDRemote; + +#pragma mark -- Delegate protocol (mandatory) -- +@protocol HIDRemoteDelegate + +// Notification of button events +- (void)hidRemote:(HIDRemote *)hidRemote // The instance of HIDRemote sending this + eventWithButton:(HIDRemoteButtonCode)buttonCode // Event for the button specified by code + isPressed:(BOOL)isPressed // The button was pressed (YES) / released (NO) + fromHardwareWithAttributes:(NSMutableDictionary *)attributes; // Information on the device this event comes from + +@optional + +// Notification of ID changes +- (void)hidRemote:(HIDRemote *)hidRemote // Invoked when the user switched to a remote control with a different ID + remoteIDChangedOldID:(SInt32)old + newID:(SInt32)newID + forHardwareWithAttributes:(NSMutableDictionary *)attributes; + +// Notification about hardware additions/removals +- (void)hidRemote:(HIDRemote *)hidRemote // Invoked when new hardware was found / added to HIDRemote's pool + foundNewHardwareWithAttributes:(NSMutableDictionary *)attributes; + +- (void)hidRemote:(HIDRemote *)hidRemote // Invoked when initialization of new hardware as requested failed + failedNewHardwareWithError:(NSError *)error; + +- (void)hidRemote:(HIDRemote *)hidRemote // Invoked when hardware was removed from HIDRemote's pool + releasedHardwareWithAttributes:(NSMutableDictionary *)attributes; + +// ### WARNING: Unless you know VERY PRECISELY what you are doing, do not implement any of the delegate methods below. ### + +// Matching of newly found receiver hardware +- (BOOL)hidRemote:(HIDRemote *)hidRemote // Invoked when new hardware is inspected + inspectNewHardwareWithService:(io_service_t)service // + prematchResult:(BOOL)prematchResult; // Return YES if HIDRemote should go on with this hardware and try + // to use it, or NO if it should not be persued further. + +// Exlusive lock lending +- (BOOL)hidRemote:(HIDRemote *)hidRemote + lendExclusiveLockToApplicationWithInfo:(NSDictionary *)applicationInfo; + +- (void)hidRemote:(HIDRemote *)hidRemote + exclusiveLockReleasedByApplicationWithInfo:(NSDictionary *)applicationInfo; + +- (BOOL)hidRemote:(HIDRemote *)hidRemote + shouldRetryExclusiveLockWithInfo:(NSDictionary *)applicationInfo; + +@end + + +#pragma mark -- Actual header file for class -- + +@interface HIDRemote : NSObject +{ + // IOMasterPort + mach_port_t _masterPort; + + // Notification ports + IONotificationPortRef _notifyPort; + CFRunLoopSourceRef _notifyRLSource; + + // Matching iterator + io_iterator_t _matchingServicesIterator; + + // SecureInput notification + io_object_t _secureInputNotification; + + // Service attributes + NSMutableDictionary *_serviceAttribMap; + + // Mode + HIDRemoteMode _mode; + BOOL _autoRecover; + NSTimer *_autoRecoveryTimer; + + // Delegate + NSObject *_delegate; + + // Last seen ID and remote model + SInt32 _lastSeenRemoteID; + HIDRemoteModel _lastSeenModel; + SInt32 _lastSeenModelRemoteID; + + // Unused button codes + NSArray *_unusedButtonCodes; + + // Simulate Plus/Minus Hold + BOOL _simulateHoldEvents; + + // SecureEventInput workaround + BOOL _secureEventInputWorkAround; + UInt64 _lastSecureEventInputPIDSum; + uid_t _lastFrontUserSession; + + // Exclusive lock lending + BOOL _exclusiveLockLending; + BOOL _sendExclusiveResourceReuseNotification; + NSNumber *_waitForReturnByPID; + NSNumber *_returnToPID; + BOOL _isRestarting; + + // Status notifications + BOOL _sendStatusNotifications; + NSString *_pidString; + + // Status + BOOL _applicationIsTerminating; + BOOL _isStopping; + + // Thread safety + #ifdef HIDREMOTE_THREADSAFETY_HARDENED_NOTIFICATION_HANDLING /* #define HIDREMOTE_THREADSAFETY_HARDENED_NOTIFICATION_HANDLING if you're running your HIDRemote instance on a background thread (requires OS X 10.5 or later) */ + NSThread *_runOnThread; + #endif +} + +#pragma mark -- PUBLIC: Shared HID Remote -- ++ (HIDRemote *)sharedHIDRemote; + +#pragma mark -- PUBLIC: System Information -- ++ (BOOL)isCandelairInstalled; ++ (BOOL)isCandelairInstallationRequiredForRemoteMode:(HIDRemoteMode)remoteMode; +- (HIDRemoteAluminumRemoteSupportLevel)aluminiumRemoteSystemSupportLevel; + +#pragma mark -- PUBLIC: Interface / API -- +- (BOOL)startRemoteControl:(HIDRemoteMode)hidRemoteMode; +- (void)stopRemoteControl; + +- (BOOL)isStarted; +- (HIDRemoteMode)startedInMode; + +- (unsigned)activeRemoteControlCount; + +- (SInt32)lastSeenRemoteControlID; + +- (void)setLastSeenModel:(HIDRemoteModel)aModel; +- (HIDRemoteModel)lastSeenModel; + +- (void)setDelegate:(NSObject *)newDelegate; +- (NSObject *)delegate; + +- (void)setSimulateHoldEvents:(BOOL)newSimulateHoldEvents; +- (BOOL)simulateHoldEvents; + +- (void)setUnusedButtonCodes:(NSArray *)newArrayWithUnusedButtonCodesAsNSNumbers; +- (NSArray *)unusedButtonCodes; + +#pragma mark -- PUBLIC: Expert APIs -- +- (void)setEnableSecureEventInputWorkaround:(BOOL)newEnableSecureEventInputWorkaround; +- (BOOL)enableSecureEventInputWorkaround; + +- (void)setExclusiveLockLendingEnabled:(BOOL)newExclusiveLockLendingEnabled; +- (BOOL)exclusiveLockLendingEnabled; + +- (BOOL)isApplicationTerminating; +- (BOOL)isStopping; + +#pragma mark -- PRIVATE: HID Event handling -- +- (void)_handleButtonCode:(HIDRemoteButtonCode)buttonCode isPressed:(BOOL)isPressed hidAttribsDict:(NSMutableDictionary *)hidAttribsDict; +- (void)_sendButtonCode:(HIDRemoteButtonCode)buttonCode isPressed:(BOOL)isPressed hidAttribsDict:(NSMutableDictionary *)hidAttribsDict; +- (void)_hidEventFor:(io_service_t)hidDevice from:(IOHIDQueueInterface **)interface withResult:(IOReturn)result; + +#pragma mark -- PRIVATE: Service setup and destruction -- +- (BOOL)_prematchService:(io_object_t)service; +- (HIDRemoteButtonCode)buttonCodeForUsage:(unsigned int)usage usagePage:(unsigned int)usagePage; +- (BOOL)_setupService:(io_object_t)service; +- (void)_destructService:(io_object_t)service; + +#pragma mark -- PRIVATE: Distributed notifiations handling -- +- (void)_postStatusWithAction:(NSString *)action; +- (void)_handleNotifications:(NSNotification *)notification; +- (void)_setSendStatusNotifications:(BOOL)doSend; +- (BOOL)_sendStatusNotifications; + +#pragma mark -- PRIVATE: Application becomes active / inactive handling for kHIDRemoteModeExclusiveAuto -- +- (void)_appStatusChanged:(NSNotification *)notification; +- (void)_delayedAutoRecovery:(NSTimer *)aTimer; + +#pragma mark -- PRIVATE: Notification handling -- +- (void)_serviceMatching:(io_iterator_t)iterator; +- (void)_serviceNotificationFor:(io_service_t)service messageType:(natural_t)messageType messageArgument:(void *)messageArgument; +- (void)_updateSessionInformation; +- (void)_secureInputNotificationFor:(io_service_t)service messageType:(natural_t)messageType messageArgument:(void *)messageArgument; + +@end + +#pragma mark -- Information attribute keys -- +extern NSString *kHIDRemoteManufacturer; +extern NSString *kHIDRemoteProduct; +extern NSString *kHIDRemoteTransport; + +#pragma mark -- Internal/Expert attribute keys (AKA: don't touch these unless you really, really, REALLY know what you do) -- +extern NSString *kHIDRemoteCFPluginInterface; +extern NSString *kHIDRemoteHIDDeviceInterface; +extern NSString *kHIDRemoteCookieButtonCodeLUT; +extern NSString *kHIDRemoteHIDQueueInterface; +extern NSString *kHIDRemoteServiceNotification; +extern NSString *kHIDRemoteCFRunLoopSource; +extern NSString *kHIDRemoteLastButtonPressed; +extern NSString *kHIDRemoteService; +extern NSString *kHIDRemoteSimulateHoldEventsTimer; +extern NSString *kHIDRemoteSimulateHoldEventsOriginButtonCode; +extern NSString *kHIDRemoteAluminumRemoteSupportLevel; +extern NSString *kHIDRemoteAluminumRemoteSupportOnDemand; + +#pragma mark -- Distributed notifications -- +extern NSString *kHIDRemoteDNHIDRemotePing; +extern NSString *kHIDRemoteDNHIDRemoteRetry; +extern NSString *kHIDRemoteDNHIDRemoteStatus; + +extern NSString *kHIDRemoteDNHIDRemoteRetryGlobalObject; + +#pragma mark -- Distributed notifications userInfo keys and values -- +extern NSString *kHIDRemoteDNStatusHIDRemoteVersionKey; +extern NSString *kHIDRemoteDNStatusPIDKey; +extern NSString *kHIDRemoteDNStatusModeKey; +extern NSString *kHIDRemoteDNStatusUnusedButtonCodesKey; +extern NSString *kHIDRemoteDNStatusRemoteControlCountKey; +extern NSString *kHIDRemoteDNStatusReturnToPIDKey; +extern NSString *kHIDRemoteDNStatusActionKey; +extern NSString *kHIDRemoteDNStatusActionStart; +extern NSString *kHIDRemoteDNStatusActionStop; +extern NSString *kHIDRemoteDNStatusActionUpdate; +extern NSString *kHIDRemoteDNStatusActionNoNeed; + +#pragma mark -- Driver compatibility flags -- +typedef enum +{ + kHIDRemoteCompatibilityFlagsStandardHIDRemoteDevice = 1L, +} HIDRemoteCompatibilityFlags; diff --git a/osdep/ar/HIDRemote.m b/osdep/ar/HIDRemote.m new file mode 100644 index 0000000000..f05628c040 --- /dev/null +++ b/osdep/ar/HIDRemote.m @@ -0,0 +1,2068 @@ +// +// HIDRemote.m +// HIDRemote V1.2 (27th May 2011) +// +// Created by Felix Schwarz on 06.04.07. +// Copyright 2007-2011 IOSPIRIT GmbH. All rights reserved. +// +// The latest version of this class is available at +// http://www.iospirit.com/developers/hidremote/ +// +// ** LICENSE ************************************************************************* +// +// Copyright (c) 2007-2011 IOSPIRIT GmbH (http://www.iospirit.com/) +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, this list +// of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, this +// list of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// * Neither the name of IOSPIRIT GmbH nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior +// written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT +// SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +// TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +// BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +// DAMAGE. +// +// ************************************************************************************ + +// ************************************************************************************ +// ********************************** DOCUMENTATION *********************************** +// ************************************************************************************ +// +// - a reference is available at http://www.iospirit.com/developers/hidremote/reference/ +// - for a guide, please see http://www.iospirit.com/developers/hidremote/guide/ +// +// ************************************************************************************ + +#import "HIDRemote.h" + +// Callback Prototypes +static void HIDEventCallback( void * target, + IOReturn result, + void * refcon, + void * sender); + +static void ServiceMatchingCallback( void *refCon, + io_iterator_t iterator); + +static void ServiceNotificationCallback(void * refCon, + io_service_t service, + natural_t messageType, + void * messageArgument); + +static void SecureInputNotificationCallback( void * refCon, + io_service_t service, + natural_t messageType, + void * messageArgument); + +// Shared HIDRemote instance +static HIDRemote *sHIDRemote = nil; + +@implementation HIDRemote + +#pragma mark -- Init, dealloc & shared instance -- + ++ (HIDRemote *)sharedHIDRemote +{ + if (sHIDRemote==nil) + { + sHIDRemote = [[HIDRemote alloc] init]; + } + + return (sHIDRemote); +} + +- (id)init +{ + if ((self = [super init]) != nil) + { + #ifdef HIDREMOTE_THREADSAFETY_HARDENED_NOTIFICATION_HANDLING + _runOnThread = [[NSThread currentThread] retain]; + #endif + + // Detect application becoming active/inactive + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appStatusChanged:) name:NSApplicationDidBecomeActiveNotification object:NSApp]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appStatusChanged:) name:NSApplicationWillResignActiveNotification object:NSApp]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appStatusChanged:) name:NSApplicationWillTerminateNotification object:NSApp]; + + // Handle distributed notifications + _pidString = [[NSString alloc] initWithFormat:@"%d", getpid()]; + + [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(_handleNotifications:) name:kHIDRemoteDNHIDRemotePing object:nil]; + [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(_handleNotifications:) name:kHIDRemoteDNHIDRemoteRetry object:kHIDRemoteDNHIDRemoteRetryGlobalObject]; + [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(_handleNotifications:) name:kHIDRemoteDNHIDRemoteRetry object:_pidString]; + + // Enabled by default: simulate hold events for plus/minus + _simulateHoldEvents = YES; + + // Enabled by default: work around for a locking issue introduced with Security Update 2008-004 / 10.4.9 and beyond (credit for finding this workaround goes to Martin Kahr) + _secureEventInputWorkAround = YES; + _secureInputNotification = 0; + + // Initialize instance variables + _lastSeenRemoteID = -1; + _lastSeenModel = kHIDRemoteModelUndetermined; + _unusedButtonCodes = [[NSMutableArray alloc] init]; + _exclusiveLockLending = NO; + _sendExclusiveResourceReuseNotification = YES; + _applicationIsTerminating = NO; + + // Send status notifications + _sendStatusNotifications = YES; + } + + return (self); +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self name:NSApplicationWillTerminateNotification object:NSApp]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:NSApplicationWillResignActiveNotification object:NSApp]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:NSApplicationDidBecomeActiveNotification object:NSApp]; + + [[NSDistributedNotificationCenter defaultCenter] removeObserver:self name:kHIDRemoteDNHIDRemotePing object:nil]; + [[NSDistributedNotificationCenter defaultCenter] removeObserver:self name:kHIDRemoteDNHIDRemoteRetry object:kHIDRemoteDNHIDRemoteRetryGlobalObject]; + [[NSDistributedNotificationCenter defaultCenter] removeObserver:self name:kHIDRemoteDNHIDRemoteRetry object:_pidString]; + [[NSDistributedNotificationCenter defaultCenter] removeObserver:self name:nil object:nil]; /* As demanded by the documentation for -[NSDistributedNotificationCenter removeObserver:name:object:] */ + + [self stopRemoteControl]; + + [self setExclusiveLockLendingEnabled:NO]; + + [self setDelegate:nil]; + + if (_unusedButtonCodes != nil) + { + [_unusedButtonCodes release]; + _unusedButtonCodes = nil; + } + + #ifdef HIDREMOTE_THREADSAFETY_HARDENED_NOTIFICATION_HANDLING + [_runOnThread release]; + _runOnThread = nil; + #endif + + [_pidString release]; + _pidString = nil; + + [super dealloc]; +} + +#pragma mark -- PUBLIC: System Information -- ++ (BOOL)isCandelairInstalled +{ + mach_port_t masterPort = 0; + kern_return_t kernResult; + io_service_t matchingService = 0; + BOOL isInstalled = NO; + + kernResult = IOMasterPort(MACH_PORT_NULL, &masterPort); + if ((kernResult!=kIOReturnSuccess) || (masterPort==0)) { return(NO); } + + if ((matchingService = IOServiceGetMatchingService(masterPort, IOServiceMatching("IOSPIRITIRController"))) != 0) + { + isInstalled = YES; + IOObjectRelease((io_object_t) matchingService); + } + + mach_port_deallocate(mach_task_self(), masterPort); + + return (isInstalled); +} + ++ (BOOL)isCandelairInstallationRequiredForRemoteMode:(HIDRemoteMode)remoteMode +{ + SInt32 systemVersion = 0; + + // Determine OS version + if (Gestalt(gestaltSystemVersion, &systemVersion) == noErr) + { + switch (systemVersion) + { + case 0x1060: // OS 10.6 + case 0x1061: // OS 10.6.1 + // OS X 10.6(.0) and OS X 10.6.1 require the Candelair driver for to be installed, + // so that third party apps can acquire an exclusive lock on the receiver HID Device + // via IOKit. + + switch (remoteMode) + { + case kHIDRemoteModeExclusive: + case kHIDRemoteModeExclusiveAuto: + if (![self isCandelairInstalled]) + { + return (YES); + } + break; + } + break; + } + } + + return (NO); +} + +- (HIDRemoteAluminumRemoteSupportLevel)aluminiumRemoteSystemSupportLevel +{ + HIDRemoteAluminumRemoteSupportLevel supportLevel = kHIDRemoteAluminumRemoteSupportLevelNone; + NSEnumerator *attribDictsEnum; + NSDictionary *hidAttribsDict; + + attribDictsEnum = [_serviceAttribMap objectEnumerator]; + + while ((hidAttribsDict = [attribDictsEnum nextObject]) != nil) + { + NSNumber *deviceSupportLevel; + + if ((deviceSupportLevel = [hidAttribsDict objectForKey:kHIDRemoteAluminumRemoteSupportLevel]) != nil) + { + if ([deviceSupportLevel intValue] > (int)supportLevel) + { + supportLevel = [deviceSupportLevel intValue]; + } + } + } + + return (supportLevel); +} + +#pragma mark -- PUBLIC: Interface / API -- +- (BOOL)startRemoteControl:(HIDRemoteMode)hidRemoteMode +{ + if ((_mode == kHIDRemoteModeNone) && (hidRemoteMode != kHIDRemoteModeNone)) + { + kern_return_t kernReturn; + CFMutableDictionaryRef matchDict=NULL; + io_service_t rootService; + + do + { + // Get IOKit master port + kernReturn = IOMasterPort(bootstrap_port, &_masterPort); + if ((kernReturn!=kIOReturnSuccess) || (_masterPort==0)) { break; } + + // Setup notification port + _notifyPort = IONotificationPortCreate(_masterPort); + + if ((_notifyRLSource = IONotificationPortGetRunLoopSource(_notifyPort)) != NULL) + { + CFRunLoopAddSource( CFRunLoopGetCurrent(), + _notifyRLSource, + kCFRunLoopCommonModes); + } + else + { + break; + } + + // Setup SecureInput notification + if ((hidRemoteMode == kHIDRemoteModeExclusive) || (hidRemoteMode == kHIDRemoteModeExclusiveAuto)) + { + if ((rootService = IORegistryEntryFromPath(_masterPort, kIOServicePlane ":/")) != 0) + { + kernReturn = IOServiceAddInterestNotification( _notifyPort, + rootService, + kIOBusyInterest, + SecureInputNotificationCallback, + (void *)self, + &_secureInputNotification); + if (kernReturn != kIOReturnSuccess) { break; } + + [self _updateSessionInformation]; + } + else + { + break; + } + } + + // Setup notification matching dict + matchDict = IOServiceMatching(kIOHIDDeviceKey); + CFRetain(matchDict); + + // Actually add notification + kernReturn = IOServiceAddMatchingNotification( _notifyPort, + kIOFirstMatchNotification, + matchDict, // one reference count consumed by this call + ServiceMatchingCallback, + (void *) self, + &_matchingServicesIterator); + if (kernReturn != kIOReturnSuccess) { break; } + + // Setup serviceAttribMap + _serviceAttribMap = [[NSMutableDictionary alloc] init]; + if (_serviceAttribMap==nil) { break; } + + // Phew .. everything went well! + _mode = hidRemoteMode; + CFRelease(matchDict); + + [self _serviceMatching:_matchingServicesIterator]; + + [self _postStatusWithAction:kHIDRemoteDNStatusActionStart]; + + return (YES); + + }while(0); + + // An error occured. Do necessary clean up. + if (matchDict!=NULL) + { + CFRelease(matchDict); + matchDict = NULL; + } + + [self stopRemoteControl]; + } + + return (NO); +} + +- (void)stopRemoteControl +{ + UInt32 serviceCount = 0; + + _autoRecover = NO; + _isStopping = YES; + + if (_autoRecoveryTimer!=nil) + { + [_autoRecoveryTimer invalidate]; + [_autoRecoveryTimer release]; + _autoRecoveryTimer = nil; + } + + if (_serviceAttribMap!=nil) + { + NSDictionary *cloneDict = [[NSDictionary alloc] initWithDictionary:_serviceAttribMap]; + + if (cloneDict!=nil) + { + NSEnumerator *mapKeyEnum = [cloneDict keyEnumerator]; + NSNumber *serviceValue; + + while ((serviceValue = [mapKeyEnum nextObject]) != nil) + { + [self _destructService:(io_object_t)[serviceValue unsignedIntValue]]; + serviceCount++; + }; + + [cloneDict release]; + cloneDict = nil; + } + + [_serviceAttribMap release]; + _serviceAttribMap = nil; + } + + if (_matchingServicesIterator!=0) + { + IOObjectRelease((io_object_t) _matchingServicesIterator); + _matchingServicesIterator = 0; + } + + if (_secureInputNotification!=0) + { + IOObjectRelease((io_object_t) _secureInputNotification); + _secureInputNotification = 0; + } + + if (_notifyRLSource!=NULL) + { + CFRunLoopSourceInvalidate(_notifyRLSource); + _notifyRLSource = NULL; + } + + if (_notifyPort!=NULL) + { + IONotificationPortDestroy(_notifyPort); + _notifyPort = NULL; + } + + if (_masterPort!=0) + { + mach_port_deallocate(mach_task_self(), _masterPort); + _masterPort = 0; + } + + if (_returnToPID!=nil) + { + [_returnToPID release]; + _returnToPID = nil; + } + + if (_mode!=kHIDRemoteModeNone) + { + // Post status + [self _postStatusWithAction:kHIDRemoteDNStatusActionStop]; + + if (_sendStatusNotifications) + { + // In case we were not ready to lend it earlier, tell other HIDRemote apps that the resources (if any were used) are now again available for use by other applications + if (((_mode==kHIDRemoteModeExclusive) || (_mode==kHIDRemoteModeExclusiveAuto)) && (_sendExclusiveResourceReuseNotification==YES) && (_exclusiveLockLending==NO) && (serviceCount>0)) + { + _mode = kHIDRemoteModeNone; + + if (!_isRestarting) + { + [[NSDistributedNotificationCenter defaultCenter] postNotificationName:kHIDRemoteDNHIDRemoteRetry + object:kHIDRemoteDNHIDRemoteRetryGlobalObject + userInfo:[NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithUnsignedInt:(unsigned int)getpid()], kHIDRemoteDNStatusPIDKey, + [[NSBundle mainBundle] bundleIdentifier], (NSString *)kCFBundleIdentifierKey, + nil] + deliverImmediately:YES]; + } + } + } + } + + _mode = kHIDRemoteModeNone; + _isStopping = NO; +} + +- (BOOL)isStarted +{ + return (_mode != kHIDRemoteModeNone); +} + +- (HIDRemoteMode)startedInMode +{ + return (_mode); +} + +- (unsigned)activeRemoteControlCount +{ + return ([_serviceAttribMap count]); +} + +- (SInt32)lastSeenRemoteControlID +{ + return (_lastSeenRemoteID); +} + +- (HIDRemoteModel)lastSeenModel +{ + return (_lastSeenModel); +} + +- (void)setLastSeenModel:(HIDRemoteModel)aModel +{ + _lastSeenModel = aModel; +} + +- (void)setSimulateHoldEvents:(BOOL)newSimulateHoldEvents +{ + _simulateHoldEvents = newSimulateHoldEvents; +} + +- (BOOL)simulateHoldEvents +{ + return (_simulateHoldEvents); +} + +- (NSArray *)unusedButtonCodes +{ + return (_unusedButtonCodes); +} + +- (void)setUnusedButtonCodes:(NSArray *)newArrayWithUnusedButtonCodesAsNSNumbers +{ + [newArrayWithUnusedButtonCodesAsNSNumbers retain]; + [_unusedButtonCodes release]; + + _unusedButtonCodes = newArrayWithUnusedButtonCodesAsNSNumbers; + + [self _postStatusWithAction:kHIDRemoteDNStatusActionUpdate]; +} + +- (void)setDelegate:(NSObject *)newDelegate +{ + _delegate = newDelegate; +} + +- (NSObject *)delegate +{ + return (_delegate); +} + +#pragma mark -- PUBLIC: Expert APIs -- +- (void)setEnableSecureEventInputWorkaround:(BOOL)newEnableSecureEventInputWorkaround +{ + _secureEventInputWorkAround = newEnableSecureEventInputWorkaround; +} + +- (BOOL)enableSecureEventInputWorkaround +{ + return (_secureEventInputWorkAround); +} + +- (void)setExclusiveLockLendingEnabled:(BOOL)newExclusiveLockLendingEnabled +{ + if (newExclusiveLockLendingEnabled != _exclusiveLockLending) + { + _exclusiveLockLending = newExclusiveLockLendingEnabled; + + if (_exclusiveLockLending) + { + [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(_handleNotifications:) name:kHIDRemoteDNHIDRemoteStatus object:nil]; + } + else + { + [[NSDistributedNotificationCenter defaultCenter] removeObserver:self name:kHIDRemoteDNHIDRemoteStatus object:nil]; + + [_waitForReturnByPID release]; + _waitForReturnByPID = nil; + } + } +} + +- (BOOL)exclusiveLockLendingEnabled +{ + return (_exclusiveLockLending); +} + +- (void)setSendExclusiveResourceReuseNotification:(BOOL)newSendExclusiveResourceReuseNotification +{ + _sendExclusiveResourceReuseNotification = newSendExclusiveResourceReuseNotification; +} + +- (BOOL)sendExclusiveResourceReuseNotification +{ + return (_sendExclusiveResourceReuseNotification); +} + +- (BOOL)isApplicationTerminating +{ + return (_applicationIsTerminating); +} + +- (BOOL)isStopping +{ + return (_isStopping); +} + +#pragma mark -- PRIVATE: Application becomes active / inactive handling for kHIDRemoteModeExclusiveAuto -- +- (void)_appStatusChanged:(NSNotification *)notification +{ + #ifdef HIDREMOTE_THREADSAFETY_HARDENED_NOTIFICATION_HANDLING + if ([self respondsToSelector:@selector(performSelector:onThread:withObject:waitUntilDone:)]) // OS X 10.5+ only + { + if ([NSThread currentThread] != _runOnThread) + { + if ([[notification name] isEqual:NSApplicationDidBecomeActiveNotification]) + { + if (!_autoRecover) + { + return; + } + } + + if ([[notification name] isEqual:NSApplicationWillResignActiveNotification]) + { + if (_mode != kHIDRemoteModeExclusiveAuto) + { + return; + } + } + + [self performSelector:@selector(_appStatusChanged:) onThread:_runOnThread withObject:notification waitUntilDone:[[notification name] isEqual:NSApplicationWillTerminateNotification]]; + return; + } + } + #endif + + if (notification!=nil) + { + if (_autoRecoveryTimer!=nil) + { + [_autoRecoveryTimer invalidate]; + [_autoRecoveryTimer release]; + _autoRecoveryTimer = nil; + } + + if ([[notification name] isEqual:NSApplicationDidBecomeActiveNotification]) + { + if (_autoRecover) + { + // Delay autorecover by 0.1 to avoid race conditions + if ((_autoRecoveryTimer = [[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:0.1] interval:0.1 target:self selector:@selector(_delayedAutoRecovery:) userInfo:nil repeats:NO]) != nil) + { + // Using CFRunLoopAddTimer instead of [[NSRunLoop currentRunLoop] addTimer:.. for consistency with run loop modes. + // The kCFRunLoopCommonModes counterpart NSRunLoopCommonModes is only available in 10.5 and later, whereas this code + // is designed to be also compatible with 10.4. CFRunLoopTimerRef is "toll-free-bridged" with NSTimer since 10.0. + CFRunLoopAddTimer(CFRunLoopGetCurrent(), (CFRunLoopTimerRef)_autoRecoveryTimer, kCFRunLoopCommonModes); + } + } + } + + if ([[notification name] isEqual:NSApplicationWillResignActiveNotification]) + { + if (_mode == kHIDRemoteModeExclusiveAuto) + { + [self stopRemoteControl]; + _autoRecover = YES; + } + } + + if ([[notification name] isEqual:NSApplicationWillTerminateNotification]) + { + _applicationIsTerminating = YES; + + if ([self isStarted]) + { + [self stopRemoteControl]; + } + } + } +} + +- (void)_delayedAutoRecovery:(NSTimer *)aTimer +{ + [_autoRecoveryTimer invalidate]; + [_autoRecoveryTimer release]; + _autoRecoveryTimer = nil; + + if (_autoRecover) + { + [self startRemoteControl:kHIDRemoteModeExclusiveAuto]; + _autoRecover = NO; + } +} + + +#pragma mark -- PRIVATE: Distributed notifiations handling -- +- (void)_postStatusWithAction:(NSString *)action +{ + if (_sendStatusNotifications) + { + [[NSDistributedNotificationCenter defaultCenter] postNotificationName:kHIDRemoteDNHIDRemoteStatus + object:((_pidString!=nil) ? _pidString : [NSString stringWithFormat:@"%d",getpid()]) + userInfo:[NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithInt:1], kHIDRemoteDNStatusHIDRemoteVersionKey, + [NSNumber numberWithUnsignedInt:(unsigned int)getpid()], kHIDRemoteDNStatusPIDKey, + [NSNumber numberWithInt:(int)_mode], kHIDRemoteDNStatusModeKey, + [NSNumber numberWithUnsignedInt:(unsigned int)[self activeRemoteControlCount]], kHIDRemoteDNStatusRemoteControlCountKey, + ((_unusedButtonCodes!=nil) ? _unusedButtonCodes : [NSArray array]), kHIDRemoteDNStatusUnusedButtonCodesKey, + action, kHIDRemoteDNStatusActionKey, + [[NSBundle mainBundle] bundleIdentifier], (NSString *)kCFBundleIdentifierKey, + _returnToPID, kHIDRemoteDNStatusReturnToPIDKey, + nil] + deliverImmediately:YES + ]; + } +} + +- (void)_handleNotifications:(NSNotification *)notification +{ + NSString *notificationName; + + #ifdef HIDREMOTE_THREADSAFETY_HARDENED_NOTIFICATION_HANDLING + if ([self respondsToSelector:@selector(performSelector:onThread:withObject:waitUntilDone:)]) // OS X 10.5+ only + { + if ([NSThread currentThread] != _runOnThread) + { + [self performSelector:@selector(_handleNotifications:) onThread:_runOnThread withObject:notification waitUntilDone:NO]; + return; + } + } + #endif + + if ((notification!=nil) && ((notificationName = [notification name]) != nil)) + { + if ([notificationName isEqual:kHIDRemoteDNHIDRemotePing]) + { + [self _postStatusWithAction:kHIDRemoteDNStatusActionUpdate]; + } + + if ([notificationName isEqual:kHIDRemoteDNHIDRemoteRetry]) + { + if ([self isStarted]) + { + BOOL retry = YES; + + // Ignore our own global retry broadcasts + if ([[notification object] isEqual:kHIDRemoteDNHIDRemoteRetryGlobalObject]) + { + NSNumber *fromPID; + + if ((fromPID = [[notification userInfo] objectForKey:kHIDRemoteDNStatusPIDKey]) != nil) + { + if (getpid() == (int)[fromPID unsignedIntValue]) + { + retry = NO; + } + } + } + + if (retry) + { + if (([self delegate] != nil) && + ([[self delegate] respondsToSelector:@selector(hidRemote:shouldRetryExclusiveLockWithInfo:)])) + { + retry = [[self delegate] hidRemote:self shouldRetryExclusiveLockWithInfo:[notification userInfo]]; + } + } + + if (retry) + { + HIDRemoteMode restartInMode = _mode; + + if (restartInMode != kHIDRemoteModeNone) + { + _isRestarting = YES; + [self stopRemoteControl]; + + [_returnToPID release]; + _returnToPID = nil; + + [self startRemoteControl:restartInMode]; + _isRestarting = NO; + + if (restartInMode != kHIDRemoteModeShared) + { + _returnToPID = [[[notification userInfo] objectForKey:kHIDRemoteDNStatusPIDKey] retain]; + } + } + } + else + { + NSNumber *cacheReturnPID = _returnToPID; + + _returnToPID = [[[notification userInfo] objectForKey:kHIDRemoteDNStatusPIDKey] retain]; + [self _postStatusWithAction:kHIDRemoteDNStatusActionNoNeed]; + [_returnToPID release]; + + _returnToPID = cacheReturnPID; + } + } + } + + if (_exclusiveLockLending) + { + if ([notificationName isEqual:kHIDRemoteDNHIDRemoteStatus]) + { + NSString *action; + + if ((action = [[notification userInfo] objectForKey:kHIDRemoteDNStatusActionKey]) != nil) + { + if ((_mode == kHIDRemoteModeNone) && (_waitForReturnByPID!=nil)) + { + NSNumber *pidNumber, *returnToPIDNumber; + + if ((pidNumber = [[notification userInfo] objectForKey:kHIDRemoteDNStatusPIDKey]) != nil) + { + returnToPIDNumber = [[notification userInfo] objectForKey:kHIDRemoteDNStatusReturnToPIDKey]; + + if ([action isEqual:kHIDRemoteDNStatusActionStart]) + { + if ([pidNumber isEqual:_waitForReturnByPID]) + { + NSNumber *startMode; + + if ((startMode = [[notification userInfo] objectForKey:kHIDRemoteDNStatusModeKey]) != nil) + { + if ([startMode intValue] == kHIDRemoteModeShared) + { + returnToPIDNumber = [NSNumber numberWithInt:getpid()]; + action = kHIDRemoteDNStatusActionNoNeed; + } + } + } + } + + if (returnToPIDNumber != nil) + { + if ([action isEqual:kHIDRemoteDNStatusActionStop] || [action isEqual:kHIDRemoteDNStatusActionNoNeed]) + { + if ([pidNumber isEqual:_waitForReturnByPID] && ([returnToPIDNumber intValue] == getpid())) + { + [_waitForReturnByPID release]; + _waitForReturnByPID = nil; + + if (([self delegate] != nil) && + ([[self delegate] respondsToSelector:@selector(hidRemote:exclusiveLockReleasedByApplicationWithInfo:)])) + { + [[self delegate] hidRemote:self exclusiveLockReleasedByApplicationWithInfo:[notification userInfo]]; + } + else + { + [self startRemoteControl:kHIDRemoteModeExclusive]; + } + } + } + } + } + } + + if (_mode==kHIDRemoteModeExclusive) + { + if ([action isEqual:kHIDRemoteDNStatusActionStart]) + { + NSNumber *originPID = [[notification userInfo] objectForKey:kHIDRemoteDNStatusPIDKey]; + BOOL lendLock = YES; + + if ([originPID intValue] != getpid()) + { + if (([self delegate] != nil) && + ([[self delegate] respondsToSelector:@selector(hidRemote:lendExclusiveLockToApplicationWithInfo:)])) + { + lendLock = [[self delegate] hidRemote:self lendExclusiveLockToApplicationWithInfo:[notification userInfo]]; + } + + if (lendLock) + { + [_waitForReturnByPID release]; + _waitForReturnByPID = [originPID retain]; + + if (_waitForReturnByPID != nil) + { + [self stopRemoteControl]; + + [[NSDistributedNotificationCenter defaultCenter] postNotificationName:kHIDRemoteDNHIDRemoteRetry + object:[NSString stringWithFormat:@"%d", [_waitForReturnByPID intValue]] + userInfo:[NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithUnsignedInt:(unsigned int)getpid()], kHIDRemoteDNStatusPIDKey, + [[NSBundle mainBundle] bundleIdentifier], (NSString *)kCFBundleIdentifierKey, + nil] + deliverImmediately:YES]; + } + } + } + } + } + } + } + } + } +} + +- (void)_setSendStatusNotifications:(BOOL)doSend +{ + _sendStatusNotifications = doSend; +} + +- (BOOL)_sendStatusNotifications +{ + return (_sendStatusNotifications); +} + +#pragma mark -- PRIVATE: Service setup and destruction -- +- (BOOL)_prematchService:(io_object_t)service +{ + BOOL serviceMatches = NO; + NSString *ioClass; + NSNumber *candelairHIDRemoteCompatibilityMask; + + if (service != 0) + { + // IOClass matching + if ((ioClass = (NSString *)IORegistryEntryCreateCFProperty((io_registry_entry_t)service, + CFSTR(kIOClassKey), + kCFAllocatorDefault, + 0)) != nil) + { + // Match on Apple's AppleIRController and old versions of the Remote Buddy IR Controller + if ([ioClass isEqual:@"AppleIRController"] || [ioClass isEqual:@"RBIOKitAIREmu"]) + { + CFTypeRef candelairHIDRemoteCompatibilityDevice; + + serviceMatches = YES; + + if ((candelairHIDRemoteCompatibilityDevice = IORegistryEntryCreateCFProperty((io_registry_entry_t)service, CFSTR("CandelairHIDRemoteCompatibilityDevice"), kCFAllocatorDefault, 0)) != NULL) + { + if (CFEqual(kCFBooleanTrue, candelairHIDRemoteCompatibilityDevice)) + { + serviceMatches = NO; + } + + CFRelease (candelairHIDRemoteCompatibilityDevice); + } + } + + // Match on the virtual IOSPIRIT IR Controller + if ([ioClass isEqual:@"IOSPIRITIRController"]) + { + serviceMatches = YES; + } + + CFRelease((CFTypeRef)ioClass); + } + + // Match on services that claim compatibility with the HID Remote class (Candelair or third-party) by having a property of CandelairHIDRemoteCompatibilityMask = 1 + if ((candelairHIDRemoteCompatibilityMask = (NSNumber *)IORegistryEntryCreateCFProperty((io_registry_entry_t)service, CFSTR("CandelairHIDRemoteCompatibilityMask"), kCFAllocatorDefault, 0)) != nil) + { + if ([candelairHIDRemoteCompatibilityMask isKindOfClass:[NSNumber class]]) + { + if ([candelairHIDRemoteCompatibilityMask unsignedIntValue] & kHIDRemoteCompatibilityFlagsStandardHIDRemoteDevice) + { + serviceMatches = YES; + } + else + { + serviceMatches = NO; + } + } + + CFRelease((CFTypeRef)candelairHIDRemoteCompatibilityMask); + } + } + + if (([self delegate]!=nil) && + ([[self delegate] respondsToSelector:@selector(hidRemote:inspectNewHardwareWithService:prematchResult:)])) + { + serviceMatches = [((NSObject *)[self delegate]) hidRemote:self inspectNewHardwareWithService:service prematchResult:serviceMatches]; + } + + return (serviceMatches); +} + +- (HIDRemoteButtonCode)buttonCodeForUsage:(unsigned int)usage usagePage:(unsigned int)usagePage +{ + HIDRemoteButtonCode buttonCode = kHIDRemoteButtonCodeNone; + + switch (usagePage) + { + case kHIDPage_Consumer: + switch (usage) + { + case kHIDUsage_Csmr_MenuPick: + // Aluminum Remote: Center + buttonCode = (kHIDRemoteButtonCodeCenter|kHIDRemoteButtonCodeAluminumMask); + break; + + case kHIDUsage_Csmr_ModeStep: + // Aluminium Remote: Center Hold + buttonCode = (kHIDRemoteButtonCodeCenterHold|kHIDRemoteButtonCodeAluminumMask); + break; + + case kHIDUsage_Csmr_PlayOrPause: + // Aluminum Remote: Play/Pause + buttonCode = (kHIDRemoteButtonCodePlay|kHIDRemoteButtonCodeAluminumMask); + break; + + case kHIDUsage_Csmr_Rewind: + buttonCode = kHIDRemoteButtonCodeLeftHold; + break; + + case kHIDUsage_Csmr_FastForward: + buttonCode = kHIDRemoteButtonCodeRightHold; + break; + + case kHIDUsage_Csmr_Menu: + buttonCode = kHIDRemoteButtonCodeMenuHold; + break; + } + break; + + case kHIDPage_GenericDesktop: + switch (usage) + { + case kHIDUsage_GD_SystemAppMenu: + buttonCode = kHIDRemoteButtonCodeMenu; + break; + + case kHIDUsage_GD_SystemMenu: + buttonCode = kHIDRemoteButtonCodeCenter; + break; + + case kHIDUsage_GD_SystemMenuRight: + buttonCode = kHIDRemoteButtonCodeRight; + break; + + case kHIDUsage_GD_SystemMenuLeft: + buttonCode = kHIDRemoteButtonCodeLeft; + break; + + case kHIDUsage_GD_SystemMenuUp: + buttonCode = kHIDRemoteButtonCodeUp; + break; + + case kHIDUsage_GD_SystemMenuDown: + buttonCode = kHIDRemoteButtonCodeDown; + break; + } + break; + + case 0x06: /* Reserved */ + switch (usage) + { + case 0x22: + buttonCode = kHIDRemoteButtonCodeIDChanged; + break; + } + break; + + case 0xFF01: /* Vendor specific */ + switch (usage) + { + case 0x23: + buttonCode = kHIDRemoteButtonCodeCenterHold; + break; + + #ifdef _HIDREMOTE_EXTENSIONS + #define _HIDREMOTE_EXTENSIONS_SECTION 2 + #include "HIDRemoteAdditions.h" + #undef _HIDREMOTE_EXTENSIONS_SECTION + #endif /* _HIDREMOTE_EXTENSIONS */ + } + break; + } + + return (buttonCode); +} + +- (BOOL)_setupService:(io_object_t)service +{ + kern_return_t kernResult; + IOReturn returnCode; + HRESULT hResult; + SInt32 score; + BOOL opened = NO, queueStarted = NO; + IOHIDDeviceInterface122 **hidDeviceInterface = NULL; + IOCFPlugInInterface **cfPluginInterface = NULL; + IOHIDQueueInterface **hidQueueInterface = NULL; + io_object_t serviceNotification = 0; + CFRunLoopSourceRef queueEventSource = NULL; + NSMutableDictionary *hidAttribsDict = nil; + CFArrayRef hidElements = NULL; + NSError *error = nil; + UInt32 errorCode = 0; + + if (![self _prematchService:service]) + { + return (NO); + } + + do + { + // Create a plugin interface .. + kernResult = IOCreatePlugInInterfaceForService( service, + kIOHIDDeviceUserClientTypeID, + kIOCFPlugInInterfaceID, + &cfPluginInterface, + &score); + + if (kernResult != kIOReturnSuccess) + { + error = [NSError errorWithDomain:NSMachErrorDomain code:kernResult userInfo:nil]; + errorCode = 1; + break; + } + + + // .. use it to get the HID interface .. + hResult = (*cfPluginInterface)->QueryInterface( cfPluginInterface, + CFUUIDGetUUIDBytes(kIOHIDDeviceInterfaceID122), + (LPVOID)&hidDeviceInterface); + + if ((hResult!=S_OK) || (hidDeviceInterface==NULL)) + { + error = [NSError errorWithDomain:NSMachErrorDomain code:hResult userInfo:nil]; + errorCode = 2; + break; + } + + + // .. then open it .. + switch (_mode) + { + case kHIDRemoteModeShared: + hResult = (*hidDeviceInterface)->open(hidDeviceInterface, kIOHIDOptionsTypeNone); + break; + + case kHIDRemoteModeExclusive: + case kHIDRemoteModeExclusiveAuto: + hResult = (*hidDeviceInterface)->open(hidDeviceInterface, kIOHIDOptionsTypeSeizeDevice); + break; + + default: + goto cleanUp; // Ugh! But there are no "double breaks" available in C AFAIK .. + break; + } + + if (hResult!=S_OK) + { + error = [NSError errorWithDomain:NSMachErrorDomain code:hResult userInfo:nil]; + errorCode = 3; + break; + } + + opened = YES; + + // .. query the HID elements .. + returnCode = (*hidDeviceInterface)->copyMatchingElements(hidDeviceInterface, + NULL, + &hidElements); + if ((returnCode != kIOReturnSuccess) || (hidElements==NULL)) + { + error = [NSError errorWithDomain:NSMachErrorDomain code:returnCode userInfo:nil]; + errorCode = 4; + + break; + } + + // Setup an event queue for HID events! + hidQueueInterface = (*hidDeviceInterface)->allocQueue(hidDeviceInterface); + if (hidQueueInterface == NULL) + { + error = [NSError errorWithDomain:NSMachErrorDomain code:kIOReturnError userInfo:nil]; + errorCode = 5; + + break; + } + + returnCode = (*hidQueueInterface)->create(hidQueueInterface, 0, 32); + if (returnCode != kIOReturnSuccess) + { + error = [NSError errorWithDomain:NSMachErrorDomain code:returnCode userInfo:nil]; + errorCode = 6; + + break; + } + + + // Setup of attributes stored for this HID device + hidAttribsDict = [[NSMutableDictionary alloc] initWithObjectsAndKeys: + [NSValue valueWithPointer:(const void *)cfPluginInterface], kHIDRemoteCFPluginInterface, + [NSValue valueWithPointer:(const void *)hidDeviceInterface], kHIDRemoteHIDDeviceInterface, + [NSValue valueWithPointer:(const void *)hidQueueInterface], kHIDRemoteHIDQueueInterface, + nil]; + + { + UInt32 i, hidElementCnt = CFArrayGetCount(hidElements); + NSMutableDictionary *cookieButtonCodeLUT = [[NSMutableDictionary alloc] init]; + NSMutableDictionary *cookieCount = [[NSMutableDictionary alloc] init]; + + if ((cookieButtonCodeLUT==nil) || (cookieCount==nil)) + { + [cookieButtonCodeLUT release]; + cookieButtonCodeLUT = nil; + + [cookieCount release]; + cookieCount = nil; + + error = [NSError errorWithDomain:NSMachErrorDomain code:kIOReturnError userInfo:nil]; + errorCode = 7; + + break; + } + + // Analyze the HID elements and find matching elements + for (i=0;iaddElement(hidQueueInterface, + (IOHIDElementCookie) [cookie unsignedIntValue], + 0); + + #ifdef _HIDREMOTE_EXTENSIONS + // Get current Apple Remote ID value + #define _HIDREMOTE_EXTENSIONS_SECTION 7 + #include "HIDRemoteAdditions.h" + #undef _HIDREMOTE_EXTENSIONS_SECTION + #endif /* _HIDREMOTE_EXTENSIONS */ + + [buttonCodeNumber release]; + [pairString release]; + } + } + } + + // Compare number of *unique* matches (thus the cookieCount dictionary) with required minimum + if ([cookieCount count] < 10) + { + [cookieButtonCodeLUT release]; + cookieButtonCodeLUT = nil; + + [cookieCount release]; + cookieCount = nil; + + error = [NSError errorWithDomain:NSMachErrorDomain code:kIOReturnError userInfo:nil]; + errorCode = 8; + + break; + } + + [hidAttribsDict setObject:cookieButtonCodeLUT forKey:kHIDRemoteCookieButtonCodeLUT]; + + [cookieButtonCodeLUT release]; + cookieButtonCodeLUT = nil; + + [cookieCount release]; + cookieCount = nil; + } + + // Finish setup of IOHIDQueueInterface with CFRunLoop + returnCode = (*hidQueueInterface)->createAsyncEventSource(hidQueueInterface, &queueEventSource); + if ((returnCode != kIOReturnSuccess) || (queueEventSource == NULL)) + { + error = [NSError errorWithDomain:NSMachErrorDomain code:returnCode userInfo:nil]; + errorCode = 9; + break; + } + + returnCode = (*hidQueueInterface)->setEventCallout(hidQueueInterface, HIDEventCallback, (void *)((intptr_t)service), (void *)self); + if (returnCode != kIOReturnSuccess) + { + error = [NSError errorWithDomain:NSMachErrorDomain code:returnCode userInfo:nil]; + errorCode = 10; + break; + } + + CFRunLoopAddSource( CFRunLoopGetCurrent(), + queueEventSource, + kCFRunLoopCommonModes); + [hidAttribsDict setObject:[NSValue valueWithPointer:(const void *)queueEventSource] forKey:kHIDRemoteCFRunLoopSource]; + + returnCode = (*hidQueueInterface)->start(hidQueueInterface); + if (returnCode != kIOReturnSuccess) + { + error = [NSError errorWithDomain:NSMachErrorDomain code:returnCode userInfo:nil]; + errorCode = 11; + break; + } + + queueStarted = YES; + + // Setup device notifications + returnCode = IOServiceAddInterestNotification( _notifyPort, + service, + kIOGeneralInterest, + ServiceNotificationCallback, + self, + &serviceNotification); + if ((returnCode != kIOReturnSuccess) || (serviceNotification==0)) + { + error = [NSError errorWithDomain:NSMachErrorDomain code:returnCode userInfo:nil]; + errorCode = 12; + break; + } + + [hidAttribsDict setObject:[NSNumber numberWithUnsignedInt:(unsigned int)serviceNotification] forKey:kHIDRemoteServiceNotification]; + + // Retain service + if (IOObjectRetain(service) != kIOReturnSuccess) + { + error = [NSError errorWithDomain:NSMachErrorDomain code:kIOReturnError userInfo:nil]; + errorCode = 13; + break; + } + + [hidAttribsDict setObject:[NSNumber numberWithUnsignedInt:(unsigned int)service] forKey:kHIDRemoteService]; + + // Get some (somewhat optional) infos on the device + { + CFStringRef product, manufacturer, transport; + + if ((product = IORegistryEntryCreateCFProperty( (io_registry_entry_t)service, + (CFStringRef) @"Product", + kCFAllocatorDefault, + 0)) != NULL) + { + if (CFGetTypeID(product) == CFStringGetTypeID()) + { + [hidAttribsDict setObject:(NSString *)product forKey:kHIDRemoteProduct]; + } + + CFRelease(product); + } + + if ((manufacturer = IORegistryEntryCreateCFProperty( (io_registry_entry_t)service, + (CFStringRef) @"Manufacturer", + kCFAllocatorDefault, + 0)) != NULL) + { + if (CFGetTypeID(manufacturer) == CFStringGetTypeID()) + { + [hidAttribsDict setObject:(NSString *)manufacturer forKey:kHIDRemoteManufacturer]; + } + + CFRelease(manufacturer); + } + + if ((transport = IORegistryEntryCreateCFProperty( (io_registry_entry_t)service, + (CFStringRef) @"Transport", + kCFAllocatorDefault, + 0)) != NULL) + { + if (CFGetTypeID(transport) == CFStringGetTypeID()) + { + [hidAttribsDict setObject:(NSString *)transport forKey:kHIDRemoteTransport]; + } + + CFRelease(transport); + } + } + + // Determine Aluminum Remote support + { + CFNumberRef aluSupport; + HIDRemoteAluminumRemoteSupportLevel supportLevel = kHIDRemoteAluminumRemoteSupportLevelNone; + + if ((_mode == kHIDRemoteModeExclusive) || (_mode == kHIDRemoteModeExclusiveAuto)) + { + // Determine if this driver offers on-demand support for the Aluminum Remote (only relevant under OS versions < 10.6.2) + if ((aluSupport = IORegistryEntryCreateCFProperty((io_registry_entry_t)service, + (CFStringRef) @"AluminumRemoteSupportLevelOnDemand", + kCFAllocatorDefault, + 0)) != nil) + { + // There is => request the driver to enable it for us + if (IORegistryEntrySetCFProperty((io_registry_entry_t)service, + CFSTR("EnableAluminumRemoteSupportForMe"), + [NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithLongLong:(long long)getpid()], @"pid", + [NSNumber numberWithLongLong:(long long)getuid()], @"uid", + nil]) == kIOReturnSuccess) + { + if (CFGetTypeID(aluSupport) == CFNumberGetTypeID()) + { + supportLevel = (HIDRemoteAluminumRemoteSupportLevel) [(NSNumber *)aluSupport intValue]; + } + + [hidAttribsDict setObject:[NSNumber numberWithBool:YES] forKey:kHIDRemoteAluminumRemoteSupportOnDemand]; + } + + CFRelease(aluSupport); + } + } + + if (supportLevel == kHIDRemoteAluminumRemoteSupportLevelNone) + { + if ((aluSupport = IORegistryEntryCreateCFProperty((io_registry_entry_t)service, + (CFStringRef) @"AluminumRemoteSupportLevel", + kCFAllocatorDefault, + 0)) != nil) + { + if (CFGetTypeID(aluSupport) == CFNumberGetTypeID()) + { + supportLevel = (HIDRemoteAluminumRemoteSupportLevel) [(NSNumber *)aluSupport intValue]; + } + + CFRelease(aluSupport); + } + else + { + CFStringRef ioKitClassName; + + if ((ioKitClassName = IORegistryEntryCreateCFProperty( (io_registry_entry_t)service, + CFSTR(kIOClassKey), + kCFAllocatorDefault, + 0)) != nil) + { + if ([(NSString *)ioKitClassName isEqual:@"AppleIRController"]) + { + SInt32 systemVersion; + + if (Gestalt(gestaltSystemVersion, &systemVersion) == noErr) + { + if (systemVersion >= 0x1062) + { + // Support for the Aluminum Remote was added only with OS 10.6.2. Previous versions can not distinguish + // between the Center and the new, seperate Play/Pause button. They'll recognize both as presses of the + // "Center" button. + // + // You CAN, however, receive Aluminum Remote button presses even under OS 10.5 when using Remote Buddy's + // Virtual Remote. While Remote Buddy does support the Aluminum Remote across all OS releases it runs on, + // its Virtual Remote can only emulate Aluminum Remote button presses under OS 10.5 and up in order not to + // break compatibility with applications whose IR Remote code relies on driver internals. [13-Nov-09] + supportLevel = kHIDRemoteAluminumRemoteSupportLevelNative; + } + } + } + + CFRelease(ioKitClassName); + } + } + } + + [hidAttribsDict setObject:(NSNumber *)[NSNumber numberWithInt:(int)supportLevel] forKey:kHIDRemoteAluminumRemoteSupportLevel]; + } + + // Add it to the serviceAttribMap + [_serviceAttribMap setObject:hidAttribsDict forKey:[NSNumber numberWithUnsignedInt:(unsigned int)service]]; + + // And we're done with setup .. + if (([self delegate]!=nil) && + ([[self delegate] respondsToSelector:@selector(hidRemote:foundNewHardwareWithAttributes:)])) + { + [((NSObject *)[self delegate]) hidRemote:self foundNewHardwareWithAttributes:hidAttribsDict]; + } + + [hidAttribsDict release]; + hidAttribsDict = nil; + + return(YES); + + }while(0); + + cleanUp: + + if (([self delegate]!=nil) && + ([[self delegate] respondsToSelector:@selector(hidRemote:failedNewHardwareWithError:)])) + { + if (error!=nil) + { + error = [NSError errorWithDomain:[error domain] + code:[error code] + userInfo:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:errorCode] forKey:@"InternalErrorCode"] + ]; + } + + [((NSObject *)[self delegate]) hidRemote:self failedNewHardwareWithError:error]; + } + + // An error occured or this device is not of interest .. cleanup .. + if (serviceNotification!=0) + { + IOObjectRelease(serviceNotification); + serviceNotification = 0; + } + + if (queueEventSource!=NULL) + { + CFRunLoopSourceInvalidate(queueEventSource); + queueEventSource=NULL; + } + + if (hidQueueInterface!=NULL) + { + if (queueStarted) + { + (*hidQueueInterface)->stop(hidQueueInterface); + } + (*hidQueueInterface)->dispose(hidQueueInterface); + (*hidQueueInterface)->Release(hidQueueInterface); + hidQueueInterface = NULL; + } + + if (hidAttribsDict!=nil) + { + [hidAttribsDict release]; + hidAttribsDict = nil; + } + + if (hidElements!=NULL) + { + CFRelease(hidElements); + hidElements = NULL; + } + + if (hidDeviceInterface!=NULL) + { + if (opened) + { + (*hidDeviceInterface)->close(hidDeviceInterface); + } + (*hidDeviceInterface)->Release(hidDeviceInterface); + // opened = NO; + hidDeviceInterface = NULL; + } + + if (cfPluginInterface!=NULL) + { + IODestroyPlugInInterface(cfPluginInterface); + cfPluginInterface = NULL; + } + + return (NO); +} + +- (void)_destructService:(io_object_t)service +{ + NSNumber *serviceValue; + NSMutableDictionary *serviceDict = NULL; + + if ((serviceValue = [NSNumber numberWithUnsignedInt:(unsigned int)service]) == nil) + { + return; + } + + serviceDict = [_serviceAttribMap objectForKey:serviceValue]; + + if (serviceDict!=nil) + { + IOHIDDeviceInterface122 **hidDeviceInterface = NULL; + IOCFPlugInInterface **cfPluginInterface = NULL; + IOHIDQueueInterface **hidQueueInterface = NULL; + io_object_t serviceNotification = 0; + CFRunLoopSourceRef queueEventSource = NULL; + io_object_t theService = 0; + NSMutableDictionary *cookieButtonMap = nil; + NSTimer *simulateHoldTimer = nil; + + serviceNotification = (io_object_t) ([serviceDict objectForKey:kHIDRemoteServiceNotification] ? [[serviceDict objectForKey:kHIDRemoteServiceNotification] unsignedIntValue] : 0); + theService = (io_object_t) ([serviceDict objectForKey:kHIDRemoteService] ? [[serviceDict objectForKey:kHIDRemoteService] unsignedIntValue] : 0); + queueEventSource = (CFRunLoopSourceRef) ([ser