xref: /AOO41X/main/apple_remote/HIDRemoteControlDevice.m (revision 53e04a5dbf8709463ce908ac78248c6a889a9b40)
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