1/***************************************************************************** 2 * HIDRemoteControlDevice.m 3 * RemoteControlWrapper 4 * 5 * Created by Martin Kahr on 11.03.06 under a MIT-style license. 6 * Copyright (c) 2006 martinkahr.com. All rights reserved. 7 * 8 * Code modified and adapted to OpenOffice.org 9 * by Eric Bachard on 11.08.2008 under the same license 10 * 11 * Permission is hereby granted, free of charge, to any person obtaining a 12 * copy of this software and associated documentation files (the "Software"), 13 * to deal in the Software without restriction, including without limitation 14 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 15 * and/or sell copies of the Software, and to permit persons to whom the 16 * Software is furnished to do so, subject to the following conditions: 17 * 18 * The above copyright notice and this permission notice shall be included 19 * in all copies or substantial portions of the Software. 20 * 21 * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 24 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 * THE SOFTWARE. 28 * 29 *****************************************************************************/ 30 31#import "HIDRemoteControlDevice.h" 32 33#import <mach/mach.h> 34#import <mach/mach_error.h> 35#import <IOKit/IOKitLib.h> 36#import <IOKit/IOCFPlugIn.h> 37#import <IOKit/hid/IOHIDKeys.h> 38#import <Carbon/Carbon.h> 39 40@interface HIDRemoteControlDevice (PrivateMethods) 41- (NSDictionary*) cookieToButtonMapping; // Creates the dictionary using the magics, depending on the remote 42- (IOHIDQueueInterface**) queue; 43- (IOHIDDeviceInterface**) hidDeviceInterface; 44- (void) handleEventWithCookieString: (NSString*) cookieString sumOfValues: (SInt32) sumOfValues; 45- (void) removeNotifcationObserver; 46- (void) remoteControlAvailable:(NSNotification *)notification; 47 48@end 49 50@interface HIDRemoteControlDevice (IOKitMethods) 51+ (io_object_t) findRemoteDevice; 52- (IOHIDDeviceInterface**) createInterfaceForDevice: (io_object_t) hidDevice; 53- (BOOL) initializeCookies; 54- (BOOL) openDevice; 55@end 56 57@implementation HIDRemoteControlDevice 58 59+ (const char*) remoteControlDeviceName { 60 return ""; 61} 62 63+ (BOOL) isRemoteAvailable { 64 io_object_t hidDevice = [self findRemoteDevice]; 65 if (hidDevice != 0) { 66 IOObjectRelease(hidDevice); 67 return YES; 68 } else { 69 return NO; 70 } 71} 72 73- (id) initWithDelegate: (id) _remoteControlDelegate { 74 if ([[self class] isRemoteAvailable] == NO) return nil; 75 76 if ( (self = [super initWithDelegate: _remoteControlDelegate]) ) { 77 openInExclusiveMode = YES; 78 queue = NULL; 79 hidDeviceInterface = NULL; 80 cookieToButtonMapping = [[NSMutableDictionary alloc] init]; 81 82 [self setCookieMappingInDictionary: cookieToButtonMapping]; 83 84 NSEnumerator* enumerator = [cookieToButtonMapping objectEnumerator]; 85 NSNumber* identifier; 86 supportedButtonEvents = 0; 87 while( (identifier = [enumerator nextObject]) ) { 88 supportedButtonEvents |= [identifier intValue]; 89 } 90 91 fixSecureEventInputBug = [[NSUserDefaults standardUserDefaults] boolForKey: @"remoteControlWrapperFixSecureEventInputBug"]; 92 } 93 94 return self; 95} 96 97- (void) dealloc { 98 [self removeNotifcationObserver]; 99 [self stopListening:self]; 100 [cookieToButtonMapping release]; 101 [super dealloc]; 102} 103 104- (void) sendRemoteButtonEvent: (RemoteControlEventIdentifier) event pressedDown: (BOOL) pressedDown { 105 [delegate sendRemoteButtonEvent: event pressedDown: pressedDown remoteControl:self]; 106} 107 108- (void) setCookieMappingInDictionary: (NSMutableDictionary*) cookieToButtonMapping { 109} 110- (int) remoteIdSwitchCookie { 111 return 0; 112} 113 114- (BOOL) sendsEventForButtonIdentifier: (RemoteControlEventIdentifier) identifier { 115 return (supportedButtonEvents & identifier) == identifier; 116} 117 118- (BOOL) isListeningToRemote { 119 return (hidDeviceInterface != NULL && allCookies != NULL && queue != NULL); 120} 121 122- (void) setListeningToRemote: (BOOL) value { 123 if (value == NO) { 124 [self stopListening:self]; 125 } else { 126 [self startListening:self]; 127 } 128} 129 130- (BOOL) isOpenInExclusiveMode { 131 return openInExclusiveMode; 132} 133- (void) setOpenInExclusiveMode: (BOOL) value { 134 openInExclusiveMode = value; 135} 136 137- (BOOL) processesBacklog { 138 return processesBacklog; 139} 140- (void) setProcessesBacklog: (BOOL) value { 141 processesBacklog = value; 142} 143 144- (void) startListening: (id) sender { 145 if ([self isListeningToRemote]) return; 146 147 // 4th July 2007 148 // 149 // A security update in february of 2007 introduced an odd behavior. 150 // Whenever SecureEventInput is activated or deactivated the exclusive access 151 // to the remote control device is lost. This leads to very strange behavior where 152 // a press on the Menu button activates FrontRow while your app still gets the event. 153 // A great number of people have complained about this. 154 // 155 // Enabling the SecureEventInput and keeping it enabled does the trick. 156 // 157 // I'm pretty sure this is a kind of bug at Apple and I'm in contact with the responsible 158 // Apple Engineer. This solution is not a perfect one - I know. 159 // One of the side effects is that applications that listen for special global keyboard shortcuts (like Quicksilver) 160 // may get into problems as they no longer get the events. 161 // As there is no official Apple Remote API from Apple I also failed to open a technical incident on this. 162 // 163 // Note that there is a corresponding DisableSecureEventInput in the stopListening method below. 164 // 165 if ([self isOpenInExclusiveMode] && fixSecureEventInputBug) EnableSecureEventInput(); 166 167 [self removeNotifcationObserver]; 168 169 io_object_t hidDevice = [[self class] findRemoteDevice]; 170 if (hidDevice == 0) return; 171 172 if ([self createInterfaceForDevice:hidDevice] == NULL) { 173 goto error; 174 } 175 176 if ([self initializeCookies]==NO) { 177 goto error; 178 } 179 180 if ([self openDevice]==NO) { 181 goto error; 182 } 183 // be KVO friendly 184 [self willChangeValueForKey:@"listeningToRemote"]; 185 [self didChangeValueForKey:@"listeningToRemote"]; 186 goto cleanup; 187 188error: 189 [self stopListening:self]; 190 DisableSecureEventInput(); 191 192cleanup: 193 IOObjectRelease(hidDevice); 194} 195 196- (void) stopListening: (id) sender { 197 if ([self isListeningToRemote]==NO) return; 198 199 BOOL sendNotification = NO; 200 201 if (eventSource != NULL) { 202 CFRunLoopRemoveSource(CFRunLoopGetCurrent(), eventSource, kCFRunLoopDefaultMode); 203 CFRelease(eventSource); 204 eventSource = NULL; 205 } 206 if (queue != NULL) { 207 (*queue)->stop(queue); 208 209 //dispose of queue 210 (*queue)->dispose(queue); 211 212 //release the queue we allocated 213 (*queue)->Release(queue); 214 215 queue = NULL; 216 217 sendNotification = YES; 218 } 219 220 if (allCookies != nil) { 221 [allCookies autorelease]; 222 allCookies = nil; 223 } 224 225 if (hidDeviceInterface != NULL) { 226 //close the device 227 (*hidDeviceInterface)->close(hidDeviceInterface); 228 229 //release the interface 230 (*hidDeviceInterface)->Release(hidDeviceInterface); 231 232 hidDeviceInterface = NULL; 233 } 234 235 if ([self isOpenInExclusiveMode] && fixSecureEventInputBug) DisableSecureEventInput(); 236 237 if ([self isOpenInExclusiveMode] && sendNotification) { 238 [[self class] sendFinishedNotifcationForAppIdentifier: nil]; 239 } 240 // be KVO friendly 241 [self willChangeValueForKey:@"listeningToRemote"]; 242 [self didChangeValueForKey:@"listeningToRemote"]; 243} 244 245@end 246 247@implementation HIDRemoteControlDevice (PrivateMethods) 248 249- (IOHIDQueueInterface**) queue { 250 return queue; 251} 252 253- (IOHIDDeviceInterface**) hidDeviceInterface { 254 return hidDeviceInterface; 255} 256 257 258- (NSDictionary*) cookieToButtonMapping { 259 return cookieToButtonMapping; 260} 261 262- (NSString*) validCookieSubstring: (NSString*) cookieString { 263 if (cookieString == nil || [cookieString length] == 0) return nil; 264 NSEnumerator* keyEnum = [[self cookieToButtonMapping] keyEnumerator]; 265 NSString* key; 266 while( (key = [keyEnum nextObject]) ) { 267 NSRange range = [cookieString rangeOfString:key]; 268 if (range.location == 0) return key; 269 } 270 return nil; 271} 272 273- (void) handleEventWithCookieString: (NSString*) cookieString sumOfValues: (SInt32) sumOfValues { 274 /* 275 if (previousRemainingCookieString) { 276 cookieString = [previousRemainingCookieString stringByAppendingString: cookieString]; 277 NSLog( @"Apple Remote: New cookie string is %@", cookieString); 278 [previousRemainingCookieString release], previousRemainingCookieString=nil; 279 }*/ 280 if (cookieString == nil || [cookieString length] == 0) return; 281 282 NSNumber* buttonId = [[self cookieToButtonMapping] objectForKey: cookieString]; 283 if (buttonId != nil) { 284 switch ( (int)buttonId ) 285 { 286 case kMetallicRemote2009ButtonPlay: 287 case kMetallicRemote2009ButtonMiddlePlay: 288 buttonId = [NSNumber numberWithInt:kRemoteButtonPlay]; 289 break; 290 default: 291 break; 292 } 293 [self sendRemoteButtonEvent: [buttonId intValue] pressedDown: (sumOfValues>0)]; 294 295 } else { 296 // let's see if a number of events are stored in the cookie string. this does 297 // happen when the main thread is too busy to handle all incoming events in time. 298 NSString* subCookieString; 299 NSString* lastSubCookieString=nil; 300 while( (subCookieString = [self validCookieSubstring: cookieString]) ) { 301 cookieString = [cookieString substringFromIndex: [subCookieString length]]; 302 lastSubCookieString = subCookieString; 303 if (processesBacklog) [self handleEventWithCookieString: subCookieString sumOfValues:sumOfValues]; 304 } 305 if (processesBacklog == NO && lastSubCookieString != nil) { 306 // process the last event of the backlog and assume that the button is not pressed down any longer. 307 // The events in the backlog do not seem to be in order and therefore (in rare cases) the last event might be 308 // a button pressed down event while in reality the user has released it. 309 // NSLog(@"processing last event of backlog"); 310 [self handleEventWithCookieString: lastSubCookieString sumOfValues:0]; 311 } 312 if ([cookieString length] > 0) { 313 NSLog( @"Apple Remote: Unknown button for cookiestring %@", cookieString); 314 } 315 } 316} 317 318- (void) removeNotifcationObserver { 319 [[NSDistributedNotificationCenter defaultCenter] removeObserver:self name:FINISHED_USING_REMOTE_CONTROL_NOTIFICATION object:nil]; 320} 321 322- (void) remoteControlAvailable:(NSNotification *)notification { 323 [self removeNotifcationObserver]; 324 [self startListening: self]; 325} 326 327@end 328 329/* Callback method for the device queue 330Will be called for any event of any type (cookie) to which we subscribe 331*/ 332static void QueueCallbackFunction(void* target, IOReturn result, void* refcon, void* sender) { 333 if (target < 0) { 334 NSLog( @"Apple Remote: QueueCallbackFunction called with invalid target!"); 335 return; 336 } 337 NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; 338 339 HIDRemoteControlDevice* remote = (HIDRemoteControlDevice*)target; 340 IOHIDEventStruct event; 341 AbsoluteTime zeroTime = {0,0}; 342 NSMutableString* cookieString = [NSMutableString string]; 343 SInt32 sumOfValues = 0; 344 while (result == kIOReturnSuccess) 345 { 346 result = (*[remote queue])->getNextEvent([remote queue], &event, zeroTime, 0); 347 if ( result != kIOReturnSuccess ) 348 continue; 349 350 //printf("%d %d %d\n", event.elementCookie, event.value, event.longValue); 351 352 if (((int)event.elementCookie)!=5) { 353 sumOfValues+=event.value; 354 [cookieString appendString:[NSString stringWithFormat:@"%d_", event.elementCookie]]; 355 } 356 } 357 [remote handleEventWithCookieString: cookieString sumOfValues: sumOfValues]; 358 359 [pool release]; 360} 361 362@implementation HIDRemoteControlDevice (IOKitMethods) 363 364- (IOHIDDeviceInterface**) createInterfaceForDevice: (io_object_t) hidDevice { 365 io_name_t className; 366 IOCFPlugInInterface** plugInInterface = NULL; 367 HRESULT plugInResult = S_OK; 368 SInt32 score = 0; 369 IOReturn ioReturnValue = kIOReturnSuccess; 370 371 hidDeviceInterface = NULL; 372 373 ioReturnValue = IOObjectGetClass(hidDevice, className); 374 375 if (ioReturnValue != kIOReturnSuccess) { 376 NSLog( @"Apple Remote: Error: Failed to get RemoteControlDevice class name."); 377 return NULL; 378 } 379 380 ioReturnValue = IOCreatePlugInInterfaceForService(hidDevice, 381 kIOHIDDeviceUserClientTypeID, 382 kIOCFPlugInInterfaceID, 383 &plugInInterface, 384 &score); 385 if (ioReturnValue == kIOReturnSuccess) 386 { 387 //Call a method of the intermediate plug-in to create the device interface 388 plugInResult = (*plugInInterface)->QueryInterface(plugInInterface, CFUUIDGetUUIDBytes(kIOHIDDeviceInterfaceID), (LPVOID) &hidDeviceInterface); 389 390 if (plugInResult != S_OK) { 391 NSLog( @"Apple Remote: Error: Couldn't create HID class device interface"); 392 } 393 // Release 394 if (plugInInterface) (*plugInInterface)->Release(plugInInterface); 395 } 396 return hidDeviceInterface; 397} 398 399- (BOOL) initializeCookies { 400 IOHIDDeviceInterface122** handle = (IOHIDDeviceInterface122**)hidDeviceInterface; 401 IOHIDElementCookie cookie; 402 long usage; 403 long usagePage; 404 id object; 405 NSArray* elements = nil; 406 NSDictionary* element; 407 IOReturn success; 408 409 if (!handle || !(*handle)) return NO; 410 411 // Copy all elements, since we're grabbing most of the elements 412 // for this device anyway, and thus, it's faster to iterate them 413 // ourselves. When grabbing only one or two elements, a matching 414 // dictionary should be passed in here instead of NULL. 415 success = (*handle)->copyMatchingElements(handle, NULL, (CFArrayRef*)&elements); 416 417 if (success == kIOReturnSuccess) { 418 419 [elements autorelease]; 420 /* 421 cookies = calloc(NUMBER_OF_APPLE_REMOTE_ACTIONS, sizeof(IOHIDElementCookie)); 422 memset(cookies, 0, sizeof(IOHIDElementCookie) * NUMBER_OF_APPLE_REMOTE_ACTIONS); 423 */ 424 allCookies = [[NSMutableArray alloc] init]; 425 426 NSEnumerator *elementsEnumerator = [elements objectEnumerator]; 427 428 while ( (element = [elementsEnumerator nextObject]) ) { 429 //Get cookie 430 object = [element valueForKey: (NSString*)CFSTR(kIOHIDElementCookieKey) ]; 431 if (object == nil || ![object isKindOfClass:[NSNumber class]]) continue; 432 if (object == 0 || CFGetTypeID(object) != CFNumberGetTypeID()) continue; 433 cookie = (IOHIDElementCookie) [object longValue]; 434 435 //Get usage 436 object = [element valueForKey: (NSString*)CFSTR(kIOHIDElementUsageKey) ]; 437 if (object == nil || ![object isKindOfClass:[NSNumber class]]) continue; 438 usage = [object longValue]; 439 440 //Get usage page 441 object = [element valueForKey: (NSString*)CFSTR(kIOHIDElementUsagePageKey) ]; 442 if (object == nil || ![object isKindOfClass:[NSNumber class]]) continue; 443 usagePage = [object longValue]; 444 445 [allCookies addObject: [NSNumber numberWithInt:(int)cookie]]; 446 } 447 } else { 448 return NO; 449 } 450 451 return YES; 452} 453 454- (BOOL) openDevice { 455 HRESULT result; 456 457 IOHIDOptionsType openMode = kIOHIDOptionsTypeNone; 458 if ([self isOpenInExclusiveMode]) openMode = kIOHIDOptionsTypeSeizeDevice; 459 IOReturn ioReturnValue = (*hidDeviceInterface)->open(hidDeviceInterface, openMode); 460 461 if (ioReturnValue == KERN_SUCCESS) { 462 queue = (*hidDeviceInterface)->allocQueue(hidDeviceInterface); 463 if (queue) { 464 result = (*queue)->create(queue, 0, 12); //depth: maximum number of elements in queue before oldest elements in queue begin to be lost. 465 466 IOHIDElementCookie cookie; 467 NSEnumerator *allCookiesEnumerator = [allCookies objectEnumerator]; 468 469 while ( (cookie = (IOHIDElementCookie)[[allCookiesEnumerator nextObject] intValue]) ) { 470 (*queue)->addElement(queue, cookie, 0); 471 } 472 473 // add callback for async events 474 ioReturnValue = (*queue)->createAsyncEventSource(queue, &eventSource); 475 if (ioReturnValue == KERN_SUCCESS) { 476 ioReturnValue = (*queue)->setEventCallout(queue,QueueCallbackFunction, self, NULL); 477 if (ioReturnValue == KERN_SUCCESS) { 478 CFRunLoopAddSource(CFRunLoopGetCurrent(), eventSource, kCFRunLoopDefaultMode); 479 480 //start data delivery to queue 481 (*queue)->start(queue); 482 return YES; 483 } else { 484 NSLog( @"Apple Remote: Error when setting event callback"); 485 } 486 } else { 487 NSLog( @"Apple Remote: Error when creating async event source"); 488 } 489 } else { 490 NSLog( @"Apple Remote: Error when opening device"); 491 } 492 } else if (ioReturnValue == kIOReturnExclusiveAccess) { 493 // the device is used exclusive by another application 494 495 // 1. we register for the FINISHED_USING_REMOTE_CONTROL_NOTIFICATION notification 496 [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(remoteControlAvailable:) name:FINISHED_USING_REMOTE_CONTROL_NOTIFICATION object:nil]; 497 498 // 2. send a distributed notification that we wanted to use the remote control 499 [[self class] sendRequestForRemoteControlNotification]; 500 } 501 return NO; 502} 503 504+ (io_object_t) findRemoteDevice { 505 CFMutableDictionaryRef hidMatchDictionary = NULL; 506 IOReturn ioReturnValue = kIOReturnSuccess; 507 io_iterator_t hidObjectIterator = 0; 508 io_object_t hidDevice = 0; 509 510 // Set up a matching dictionary to search the I/O Registry by class 511 // name for all HID class devices 512 hidMatchDictionary = IOServiceMatching([self remoteControlDeviceName]); 513 514 // Now search I/O Registry for matching devices. 515 ioReturnValue = IOServiceGetMatchingServices(kIOMasterPortDefault, hidMatchDictionary, &hidObjectIterator); 516 517 if ((ioReturnValue == kIOReturnSuccess) && (hidObjectIterator != 0)) { 518 hidDevice = IOIteratorNext(hidObjectIterator); 519 } 520 521 // release the iterator 522 IOObjectRelease(hidObjectIterator); 523 524 return hidDevice; 525} 526 527@end 528 529