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