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