merge the formfield patch from ooo-build
[ooovba.git] / apple_remote / HIDRemoteControlDevice.m
blob94215900717bedb8337fc85472ba360a332a93a5
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  *****************************************************************************/
31 #import "HIDRemoteControlDevice.h"
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>
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;
48 @end
50 @interface HIDRemoteControlDevice (IOKitMethods) 
51 + (io_object_t) findRemoteDevice;
52 - (IOHIDDeviceInterface**) createInterfaceForDevice: (io_object_t) hidDevice;
53 - (BOOL) initializeCookies;
54 - (BOOL) openDevice;
55 @end
57 @implementation HIDRemoteControlDevice
59 + (const char*) remoteControlDeviceName {
60         return "";
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         }
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];
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;
97 - (void) dealloc {
98         [self removeNotifcationObserver];
99         [self stopListening:self];
100         [cookieToButtonMapping release];
101         [super dealloc];
104 - (void) sendRemoteButtonEvent: (RemoteControlEventIdentifier) event pressedDown: (BOOL) pressedDown {
105         [delegate sendRemoteButtonEvent: event pressedDown: pressedDown remoteControl:self];
108 - (void) setCookieMappingInDictionary: (NSMutableDictionary*) cookieToButtonMapping {
110 - (int) remoteIdSwitchCookie {
111         return 0;
114 - (BOOL) sendsEventForButtonIdentifier: (RemoteControlEventIdentifier) identifier {
115         return (supportedButtonEvents & identifier) == identifier;
117         
118 - (BOOL) isListeningToRemote {
119         return (hidDeviceInterface != NULL && allCookies != NULL && queue != NULL);     
122 - (void) setListeningToRemote: (BOOL) value {
123         if (value == NO) {
124                 [self stopListening:self];
125         } else {
126                 [self startListening:self];
127         }
130 - (BOOL) isOpenInExclusiveMode {
131         return openInExclusiveMode;
133 - (void) setOpenInExclusiveMode: (BOOL) value {
134         openInExclusiveMode = value;
137 - (BOOL) processesBacklog {
138         return processesBacklog;
140 - (void) setProcessesBacklog: (BOOL) value {
141         processesBacklog = value;
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         }
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         
188 error:
189         [self stopListening:self];
190         DisableSecureEventInput();
191         
192 cleanup:        
193         IOObjectRelease(hidDevice);     
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"];       
245 @end
247 @implementation HIDRemoteControlDevice (PrivateMethods) 
249 - (IOHIDQueueInterface**) queue {
250         return queue;
253 - (IOHIDDeviceInterface**) hidDeviceInterface {
254         return hidDeviceInterface;
258 - (NSDictionary*) cookieToButtonMapping {
259         return cookieToButtonMapping;
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;
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         }
308 - (void) removeNotifcationObserver {
309         [[NSDistributedNotificationCenter defaultCenter] removeObserver:self name:FINISHED_USING_REMOTE_CONTROL_NOTIFICATION object:nil];
312 - (void) remoteControlAvailable:(NSNotification *)notification {
313         [self removeNotifcationObserver];
314         [self startListening: self];
317 @end
319 /*      Callback method for the device queue
320 Will be called for any event of any type (cookie) to which we subscribe
322 static 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];
352 @implementation HIDRemoteControlDevice (IOKitMethods)
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;
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];
435                         [allCookies addObject: [NSNumber numberWithInt:(int)cookie]];
436                 }
437         } else {
438                 return NO;
439         }
440         
441         return YES;
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.
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;                              
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;
517 @end