Revert "macosx: clean code of objectAtIndex"
[vlc/vlc-acra.git] / modules / gui / macosx / SPMediaKeyTap.m
blobdaf7b0218b89b7cc4aeb25c63c7688e78fcd233d
1 /*
2  Copyright (c) 2011, Joachim Bengtsson
3  All rights reserved.
5  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
7  * Neither the name of the organization nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
9  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
12 // Copyright (c) 2010 Spotify AB
13 #import "SPMediaKeyTap.h"
14 #import "SPInvocationGrabbing.h"
16 @interface SPMediaKeyTap ()
17 -(BOOL)shouldInterceptMediaKeyEvents;
18 -(void)setShouldInterceptMediaKeyEvents:(BOOL)newSetting;
19 -(void)startWatchingAppSwitching;
20 -(void)stopWatchingAppSwitching;
21 -(void)eventTapThread;
22 @end
23 static SPMediaKeyTap *singleton = nil;
25 static pascal OSStatus appSwitched (EventHandlerCallRef nextHandler, EventRef evt, void* userData);
26 static pascal OSStatus appTerminated (EventHandlerCallRef nextHandler, EventRef evt, void* userData);
27 static CGEventRef tapEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon);
30 // Inspired by http://gist.github.com/546311
32 @implementation SPMediaKeyTap
34 #pragma mark -
35 #pragma mark Setup and teardown
36 -(id)initWithDelegate:(id)delegate;
38     _delegate = delegate;
39     [self startWatchingAppSwitching];
40     singleton = self;
41     _mediaKeyAppList = [NSMutableArray new];
42     _tapThreadRL=nil;
43     _eventPort=nil;
44     _eventPortSource=nil;
45     return self;
47 -(void)dealloc;
49     [self stopWatchingMediaKeys];
50     [self stopWatchingAppSwitching];
51     [_mediaKeyAppList release];
52     [super dealloc];
55 -(void)startWatchingAppSwitching;
57     // Listen to "app switched" event, so that we don't intercept media keys if we
58     // weren't the last "media key listening" app to be active
59     EventTypeSpec eventType = { kEventClassApplication, kEventAppFrontSwitched };
60     OSStatus err = InstallApplicationEventHandler(NewEventHandlerUPP(appSwitched), 1, &eventType, self, &_app_switching_ref);
61     assert(err == noErr);
63     eventType.eventKind = kEventAppTerminated;
64     err = InstallApplicationEventHandler(NewEventHandlerUPP(appTerminated), 1, &eventType, self, &_app_terminating_ref);
65     assert(err == noErr);
67 -(void)stopWatchingAppSwitching;
69     if(!_app_switching_ref) return;
70     RemoveEventHandler(_app_switching_ref);
71     _app_switching_ref = NULL;
74 -(void)startWatchingMediaKeys;{
75     // Prevent having multiple mediaKeys threads
76     [self stopWatchingMediaKeys];
78     [self setShouldInterceptMediaKeyEvents:YES];
80     // Add an event tap to intercept the system defined media key events
81     _eventPort = CGEventTapCreate(kCGSessionEventTap,
82                                   kCGHeadInsertEventTap,
83                                   kCGEventTapOptionDefault,
84                                   CGEventMaskBit(NX_SYSDEFINED),
85                                   tapEventCallback,
86                                   self);
87     assert(_eventPort != NULL);
89     _eventPortSource = CFMachPortCreateRunLoopSource(kCFAllocatorSystemDefault, _eventPort, 0);
90     assert(_eventPortSource != NULL);
92     // Let's do this in a separate thread so that a slow app doesn't lag the event tap
93     [NSThread detachNewThreadSelector:@selector(eventTapThread) toTarget:self withObject:nil];
95 -(void)stopWatchingMediaKeys;
97     // TODO<nevyn>: Shut down thread, remove event tap port and source
99     if(_tapThreadRL){
100         CFRunLoopStop(_tapThreadRL);
101         _tapThreadRL=nil;
102     }
104     if(_eventPort){
105         CFMachPortInvalidate(_eventPort);
106         CFRelease(_eventPort);
107         _eventPort=nil;
108     }
110     if(_eventPortSource){
111         CFRelease(_eventPortSource);
112         _eventPortSource=nil;
113     }
116 #pragma mark -
117 #pragma mark Accessors
119 +(BOOL)usesGlobalMediaKeyTap
121 #ifdef _DEBUG
122     // breaking in gdb with a key tap inserted sometimes locks up all mouse and keyboard input forever, forcing reboot
123     return NO;
124 #else
125     // XXX(nevyn): MediaKey event tap doesn't work on 10.4, feel free to figure out why if you have the energy.
126     return
127         ![[NSUserDefaults standardUserDefaults] boolForKey:kIgnoreMediaKeysDefaultsKey]
128         && floor(NSAppKitVersionNumber) >= 949/*NSAppKitVersionNumber10_5*/;
129 #endif
132 + (NSArray*)defaultMediaKeyUserBundleIdentifiers;
134     return @[[[NSBundle mainBundle] bundleIdentifier], // your app
135              @"com.spotify.client",
136              @"com.apple.iTunes",
137              @"com.apple.QuickTimePlayerX",
138              @"com.apple.quicktimeplayer",
139              @"com.apple.iWork.Keynote",
140              @"com.apple.iPhoto",
141              @"org.videolan.vlc",
142              @"com.apple.Aperture",
143              @"com.plexsquared.Plex",
144              @"com.soundcloud.desktop",
145              @"org.niltsh.MPlayerX",
146              @"com.ilabs.PandorasHelper",
147              @"com.mahasoftware.pandabar",
148              @"com.bitcartel.pandorajam",
149              @"org.clementine-player.clementine",
150              @"fm.last.Last.fm",
151              @"com.beatport.BeatportPro",
152              @"com.Timenut.SongKey",
153              @"com.macromedia.fireworks", // the tap messes up their mouse input
154     ];
158 -(BOOL)shouldInterceptMediaKeyEvents;
160     BOOL shouldIntercept = NO;
161     @synchronized(self) {
162         shouldIntercept = _shouldInterceptMediaKeyEvents;
163     }
164     return shouldIntercept;
167 -(void)pauseTapOnTapThread:(BOOL)yeahno;
169     CGEventTapEnable(self->_eventPort, yeahno);
171 -(void)setShouldInterceptMediaKeyEvents:(BOOL)newSetting;
173     BOOL oldSetting;
174     @synchronized(self) {
175         oldSetting = _shouldInterceptMediaKeyEvents;
176         _shouldInterceptMediaKeyEvents = newSetting;
177     }
178     if(_tapThreadRL && oldSetting != newSetting) {
179         id grab = [self grab];
180         [grab pauseTapOnTapThread:newSetting];
181         NSTimer *timer = [NSTimer timerWithTimeInterval:0 invocation:[grab invocation] repeats:NO];
182         CFRunLoopAddTimer(_tapThreadRL, (CFRunLoopTimerRef)timer, kCFRunLoopCommonModes);
183     }
186 #pragma mark
187 #pragma mark -
188 #pragma mark Event tap callbacks
190 // Note: method called on background thread
192 static CGEventRef tapEventCallback2(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
194     SPMediaKeyTap *self = refcon;
196     if(type == kCGEventTapDisabledByTimeout) {
197         NSLog(@"Media key event tap was disabled by timeout");
198         CGEventTapEnable(self->_eventPort, TRUE);
199         return event;
200     } else if(type == kCGEventTapDisabledByUserInput) {
201         // Was disabled manually by -[pauseTapOnTapThread]
202         return event;
203     }
204     NSEvent *nsEvent = nil;
205     @try {
206         nsEvent = [NSEvent eventWithCGEvent:event];
207     }
208     @catch (NSException * e) {
209         NSLog(@"Strange CGEventType: %d: %@", type, e);
210         assert(0);
211         return event;
212     }
214     if (type != NX_SYSDEFINED || [nsEvent subtype] != SPSystemDefinedEventMediaKeys)
215         return event;
217     int keyCode = (([nsEvent data1] & 0xFFFF0000) >> 16);
218     if (keyCode != NX_KEYTYPE_PLAY && keyCode != NX_KEYTYPE_FAST && keyCode != NX_KEYTYPE_REWIND && keyCode != NX_KEYTYPE_PREVIOUS && keyCode != NX_KEYTYPE_NEXT)
219         return event;
221     if (![self shouldInterceptMediaKeyEvents])
222         return event;
224     [nsEvent retain]; // matched in handleAndReleaseMediaKeyEvent:
225     [self performSelectorOnMainThread:@selector(handleAndReleaseMediaKeyEvent:) withObject:nsEvent waitUntilDone:NO];
227     return NULL;
230 static CGEventRef tapEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
232     NSAutoreleasePool *pool = [NSAutoreleasePool new];
233     CGEventRef ret = tapEventCallback2(proxy, type, event, refcon);
234     [pool drain];
235     return ret;
239 // event will have been retained in the other thread
240 -(void)handleAndReleaseMediaKeyEvent:(NSEvent *)event {
241     [event autorelease];
243     [_delegate mediaKeyTap:self receivedMediaKeyEvent:event];
247 -(void)eventTapThread;
249     _tapThreadRL = CFRunLoopGetCurrent();
250     CFRunLoopAddSource(_tapThreadRL, _eventPortSource, kCFRunLoopCommonModes);
251     CFRunLoopRun();
254 #pragma mark Task switching callbacks
256 NSString *kMediaKeyUsingBundleIdentifiersDefaultsKey = @"SPApplicationsNeedingMediaKeys";
257 NSString *kIgnoreMediaKeysDefaultsKey = @"SPIgnoreMediaKeys";
261 -(void)mediaKeyAppListChanged;
263     if([_mediaKeyAppList count] == 0) return;
265     /*NSLog(@"--");
266     int i = 0;
267     for (NSValue *psnv in _mediaKeyAppList) {
268         ProcessSerialNumber psn; [psnv getValue:&psn];
269         NSDictionary *processInfo = [(id)ProcessInformationCopyDictionary(
270             &psn,
271             kProcessDictionaryIncludeAllInformationMask
272         ) autorelease];
273         NSString *bundleIdentifier = [processInfo objectForKey:(id)kCFBundleIdentifierKey];
274         NSLog(@"%d: %@", i++, bundleIdentifier);
275     }*/
277     ProcessSerialNumber mySerial, topSerial;
278     GetCurrentProcess(&mySerial);
279     [[_mediaKeyAppList objectAtIndex:0] getValue:&topSerial];
281     Boolean same;
282     OSErr err = SameProcess(&mySerial, &topSerial, &same);
283     [self setShouldInterceptMediaKeyEvents:(err == noErr && same)];
285 -(void)appIsNowFrontmost:(ProcessSerialNumber)psn;
287     NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)];
289     NSDictionary *processInfo = [(id)ProcessInformationCopyDictionary(
290         &psn,
291         kProcessDictionaryIncludeAllInformationMask
292     ) autorelease];
293     NSString *bundleIdentifier = [processInfo objectForKey:(id)kCFBundleIdentifierKey];
295     NSArray *whitelistIdentifiers = [[NSUserDefaults standardUserDefaults] arrayForKey:kMediaKeyUsingBundleIdentifiersDefaultsKey];
296     if(![whitelistIdentifiers containsObject:bundleIdentifier]) return;
298     [_mediaKeyAppList removeObject:psnv];
299     [_mediaKeyAppList insertObject:psnv atIndex:0];
300     [self mediaKeyAppListChanged];
302 -(void)appTerminated:(ProcessSerialNumber)psn;
304     NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)];
305     [_mediaKeyAppList removeObject:psnv];
306     [self mediaKeyAppListChanged];
309 static pascal OSStatus appSwitched (EventHandlerCallRef nextHandler, EventRef evt, void* userData)
311     SPMediaKeyTap *self = (id)userData;
313     ProcessSerialNumber newSerial;
314     GetFrontProcess(&newSerial);
316     [self appIsNowFrontmost:newSerial];
318     return CallNextEventHandler(nextHandler, evt);
321 static pascal OSStatus appTerminated (EventHandlerCallRef nextHandler, EventRef evt, void* userData)
323     SPMediaKeyTap *self = (id)userData;
325     ProcessSerialNumber deadPSN;
327     GetEventParameter(
328         evt,
329         kEventParamProcessID,
330         typeProcessSerialNumber,
331         NULL,
332         sizeof(deadPSN),
333         NULL,
334         &deadPSN
335     );
337     [self appTerminated:deadPSN];
338     return CallNextEventHandler(nextHandler, evt);
341 @end