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(@"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 [self sendRemoteButtonEvent: [buttonId intValue] pressedDown: (sumOfValues>0)]; 285 } else { 286 // let's see if a number of events are stored in the cookie string. this does 287 // happen when the main thread is too busy to handle all incoming events in time. 288 NSString* subCookieString; 289 NSString* lastSubCookieString=nil; 290 while( (subCookieString = [self validCookieSubstring: cookieString]) ) { 291 cookieString = [cookieString substringFromIndex: [subCookieString length]]; 292 lastSubCookieString = subCookieString; 293 if (processesBacklog) [self handleEventWithCookieString: subCookieString sumOfValues:sumOfValues]; 294 } 295 if (processesBacklog == NO && lastSubCookieString != nil) { 296 // process the last event of the backlog and assume that the button is not pressed down any longer. 297 // The events in the backlog do not seem to be in order and therefore (in rare cases) the last event might be 298 // a button pressed down event while in reality the user has released it. 299 // NSLog(@"processing last event of backlog"); 300 [self handleEventWithCookieString: lastSubCookieString sumOfValues:0]; 301 } 302 if ([cookieString length] > 0) { 303 NSLog(@"Unknown button for cookiestring %@", cookieString); 304 } 305 } 306} 307 308- (void) removeNotifcationObserver { 309 [[NSDistributedNotificationCenter defaultCenter] removeObserver:self name:FINISHED_USING_REMOTE_CONTROL_NOTIFICATION object:nil]; 310} 311 312- (void) remoteControlAvailable:(NSNotification *)notification { 313 [self removeNotifcationObserver]; 314 [self startListening: self]; 315} 316 317@end 318 319/* Callback method for the device queue 320Will be called for any event of any type (cookie) to which we subscribe 321*/ 322static void QueueCallbackFunction(void* target, IOReturn result, void* refcon, void* sender) { 323 if (target < 0) { 324 NSLog(@"QueueCallbackFunction called with invalid target!"); 325 return; 326 } 327 NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; 328 329 HIDRemoteControlDevice* remote = (HIDRemoteControlDevice*)target; 330 IOHIDEventStruct event; 331 AbsoluteTime zeroTime = {0,0}; 332 NSMutableString* cookieString = [NSMutableString string]; 333 SInt32 sumOfValues = 0; 334 while (result == kIOReturnSuccess) 335 { 336 result = (*[remote queue])->getNextEvent([remote queue], &event, zeroTime, 0); 337 if ( result != kIOReturnSuccess ) 338 continue; 339 340 //printf("%d %d %d\n", event.elementCookie, event.value, event.longValue); 341 342 if (((int)event.elementCookie)!=5) { 343 sumOfValues+=event.value; 344 [cookieString appendString:[NSString stringWithFormat:@"%d_", event.elementCookie]]; 345 } 346 } 347 [remote handleEventWithCookieString: cookieString sumOfValues: sumOfValues]; 348 349 [pool release]; 350} 351 352@implementation HIDRemoteControlDevice (IOKitMethods) 353 354- (IOHIDDeviceInterface**) createInterfaceForDevice: (io_object_t) hidDevice { 355 io_name_t className; 356 IOCFPlugInInterface** plugInInterface = NULL; 357 HRESULT plugInResult = S_OK; 358 SInt32 score = 0; 359 IOReturn ioReturnValue = kIOReturnSuccess; 360 361 hidDeviceInterface = NULL; 362 363 ioReturnValue = IOObjectGetClass(hidDevice, className); 364 365 if (ioReturnValue != kIOReturnSuccess) { 366 NSLog(@"Error: Failed to get class name."); 367 return NULL; 368 } 369 370 ioReturnValue = IOCreatePlugInInterfaceForService(hidDevice, 371 kIOHIDDeviceUserClientTypeID, 372 kIOCFPlugInInterfaceID, 373 &plugInInterface, 374 &score); 375 if (ioReturnValue == kIOReturnSuccess) 376 { 377 //Call a method of the intermediate plug-in to create the device interface 378 plugInResult = (*plugInInterface)->QueryInterface(plugInInterface, CFUUIDGetUUIDBytes(kIOHIDDeviceInterfaceID), (LPVOID) &hidDeviceInterface); 379 380 if (plugInResult != S_OK) { 381 NSLog(@"Error: Couldn't create HID class device interface"); 382 } 383 // Release 384 if (plugInInterface) (*plugInInterface)->Release(plugInInterface); 385 } 386 return hidDeviceInterface; 387} 388 389- (BOOL) initializeCookies { 390 IOHIDDeviceInterface122** handle = (IOHIDDeviceInterface122**)hidDeviceInterface; 391 IOHIDElementCookie cookie; 392 long usage; 393 long usagePage; 394 id object; 395 NSArray* elements = nil; 396 NSDictionary* element; 397 IOReturn success; 398 399 if (!handle || !(*handle)) return NO; 400 401 // Copy all elements, since we're grabbing most of the elements 402 // for this device anyway, and thus, it's faster to iterate them 403 // ourselves. When grabbing only one or two elements, a matching 404 // dictionary should be passed in here instead of NULL. 405 success = (*handle)->copyMatchingElements(handle, NULL, (CFArrayRef*)&elements); 406 407 if (success == kIOReturnSuccess) { 408 409 [elements autorelease]; 410 /* 411 cookies = calloc(NUMBER_OF_APPLE_REMOTE_ACTIONS, sizeof(IOHIDElementCookie)); 412 memset(cookies, 0, sizeof(IOHIDElementCookie) * NUMBER_OF_APPLE_REMOTE_ACTIONS); 413 */ 414 allCookies = [[NSMutableArray alloc] init]; 415 416 NSEnumerator *elementsEnumerator = [elements objectEnumerator]; 417 418 while ( (element = [elementsEnumerator nextObject]) ) { 419 //Get cookie 420 object = [element valueForKey: (NSString*)CFSTR(kIOHIDElementCookieKey) ]; 421 if (object == nil || ![object isKindOfClass:[NSNumber class]]) continue; 422 if (object == 0 || CFGetTypeID(object) != CFNumberGetTypeID()) continue; 423 cookie = (IOHIDElementCookie) [object longValue]; 424 425 //Get usage 426 object = [element valueForKey: (NSString*)CFSTR(kIOHIDElementUsageKey) ]; 427 if (object == nil || ![object isKindOfClass:[NSNumber class]]) continue; 428 usage = [object longValue]; 429 430 //Get usage page 431 object = [element valueForKey: (NSString*)CFSTR(kIOHIDElementUsagePageKey) ]; 432 if (object == nil || ![object isKindOfClass:[NSNumber class]]) continue; 433 usagePage = [object longValue]; 434 435 [allCookies addObject: [NSNumber numberWithInt:(int)cookie]]; 436 } 437 } else { 438 return NO; 439 } 440 441 return YES; 442} 443 444- (BOOL) openDevice { 445 HRESULT result; 446 447 IOHIDOptionsType openMode = kIOHIDOptionsTypeNone; 448 if ([self isOpenInExclusiveMode]) openMode = kIOHIDOptionsTypeSeizeDevice; 449 IOReturn ioReturnValue = (*hidDeviceInterface)->open(hidDeviceInterface, openMode); 450 451 if (ioReturnValue == KERN_SUCCESS) { 452 queue = (*hidDeviceInterface)->allocQueue(hidDeviceInterface); 453 if (queue) { 454 result = (*queue)->create(queue, 0, 12); //depth: maximum number of elements in queue before oldest elements in queue begin to be lost. 455 456 IOHIDElementCookie cookie; 457 NSEnumerator *allCookiesEnumerator = [allCookies objectEnumerator]; 458 459 while ( (cookie = (IOHIDElementCookie)[[allCookiesEnumerator nextObject] intValue]) ) { 460 (*queue)->addElement(queue, cookie, 0); 461 } 462 463 // add callback for async events 464 ioReturnValue = (*queue)->createAsyncEventSource(queue, &eventSource); 465 if (ioReturnValue == KERN_SUCCESS) { 466 ioReturnValue = (*queue)->setEventCallout(queue,QueueCallbackFunction, self, NULL); 467 if (ioReturnValue == KERN_SUCCESS) { 468 CFRunLoopAddSource(CFRunLoopGetCurrent(), eventSource, kCFRunLoopDefaultMode); 469 470 //start data delivery to queue 471 (*queue)->start(queue); 472 return YES; 473 } else { 474 NSLog(@"Error when setting event callback"); 475 } 476 } else { 477 NSLog(@"Error when creating async event source"); 478 } 479 } else { 480 NSLog(@"Error when opening device"); 481 } 482 } else if (ioReturnValue == kIOReturnExclusiveAccess) { 483 // the device is used exclusive by another application 484 485 // 1. we register for the FINISHED_USING_REMOTE_CONTROL_NOTIFICATION notification 486 [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(remoteControlAvailable:) name:FINISHED_USING_REMOTE_CONTROL_NOTIFICATION object:nil]; 487 488 // 2. send a distributed notification that we wanted to use the remote control 489 [[self class] sendRequestForRemoteControlNotification]; 490 } 491 return NO; 492} 493 494+ (io_object_t) findRemoteDevice { 495 CFMutableDictionaryRef hidMatchDictionary = NULL; 496 IOReturn ioReturnValue = kIOReturnSuccess; 497 io_iterator_t hidObjectIterator = 0; 498 io_object_t hidDevice = 0; 499 500 // Set up a matching dictionary to search the I/O Registry by class 501 // name for all HID class devices 502 hidMatchDictionary = IOServiceMatching([self remoteControlDeviceName]); 503 504 // Now search I/O Registry for matching devices. 505 ioReturnValue = IOServiceGetMatchingServices(kIOMasterPortDefault, hidMatchDictionary, &hidObjectIterator); 506 507 if ((ioReturnValue == kIOReturnSuccess) && (hidObjectIterator != 0)) { 508 hidDevice = IOIteratorNext(hidObjectIterator); 509 } 510 511 // release the iterator 512 IOObjectRelease(hidObjectIterator); 513 514 return hidDevice; 515} 516 517@end 518 519