2 Copyright (c) 2011, Joachim Bengtsson
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;
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
35 #pragma mark Setup and teardown
36 -(id)initWithDelegate:(id)delegate;
39 [self startWatchingAppSwitching];
41 _mediaKeyAppList = [NSMutableArray new];
49 [self stopWatchingMediaKeys];
50 [self stopWatchingAppSwitching];
51 [_mediaKeyAppList release];
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);
63 eventType.eventKind = kEventAppTerminated;
64 err = InstallApplicationEventHandler(NewEventHandlerUPP(appTerminated), 1, &eventType, self, &_app_terminating_ref);
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),
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
100 CFRunLoopStop(_tapThreadRL);
105 CFMachPortInvalidate(_eventPort);
106 CFRelease(_eventPort);
110 if(_eventPortSource){
111 CFRelease(_eventPortSource);
112 _eventPortSource=nil;
117 #pragma mark Accessors
119 +(BOOL)usesGlobalMediaKeyTap
122 // breaking in gdb with a key tap inserted sometimes locks up all mouse and keyboard input forever, forcing reboot
125 // XXX(nevyn): MediaKey event tap doesn't work on 10.4, feel free to figure out why if you have the energy.
127 ![[NSUserDefaults standardUserDefaults] boolForKey:kIgnoreMediaKeysDefaultsKey]
128 && floor(NSAppKitVersionNumber) >= 949/*NSAppKitVersionNumber10_5*/;
132 + (NSArray*)defaultMediaKeyUserBundleIdentifiers;
134 return @[[[NSBundle mainBundle] bundleIdentifier], // your app
135 @"com.spotify.client",
137 @"com.apple.QuickTimePlayerX",
138 @"com.apple.quicktimeplayer",
139 @"com.apple.iWork.Keynote",
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",
151 @"com.beatport.BeatportPro",
152 @"com.Timenut.SongKey",
153 @"com.macromedia.fireworks", // the tap messes up their mouse input
158 -(BOOL)shouldInterceptMediaKeyEvents;
160 BOOL shouldIntercept = NO;
161 @synchronized(self) {
162 shouldIntercept = _shouldInterceptMediaKeyEvents;
164 return shouldIntercept;
167 -(void)pauseTapOnTapThread:(BOOL)yeahno;
169 CGEventTapEnable(self->_eventPort, yeahno);
171 -(void)setShouldInterceptMediaKeyEvents:(BOOL)newSetting;
174 @synchronized(self) {
175 oldSetting = _shouldInterceptMediaKeyEvents;
176 _shouldInterceptMediaKeyEvents = newSetting;
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);
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);
200 } else if(type == kCGEventTapDisabledByUserInput) {
201 // Was disabled manually by -[pauseTapOnTapThread]
204 NSEvent *nsEvent = nil;
206 nsEvent = [NSEvent eventWithCGEvent:event];
208 @catch (NSException * e) {
209 NSLog(@"Strange CGEventType: %d: %@", type, e);
214 if (type != NX_SYSDEFINED || [nsEvent subtype] != SPSystemDefinedEventMediaKeys)
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)
221 if (![self shouldInterceptMediaKeyEvents])
224 [nsEvent retain]; // matched in handleAndReleaseMediaKeyEvent:
225 [self performSelectorOnMainThread:@selector(handleAndReleaseMediaKeyEvent:) withObject:nsEvent waitUntilDone:NO];
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);
239 // event will have been retained in the other thread
240 -(void)handleAndReleaseMediaKeyEvent:(NSEvent *)event {
243 [_delegate mediaKeyTap:self receivedMediaKeyEvent:event];
247 -(void)eventTapThread;
249 _tapThreadRL = CFRunLoopGetCurrent();
250 CFRunLoopAddSource(_tapThreadRL, _eventPortSource, kCFRunLoopCommonModes);
254 #pragma mark Task switching callbacks
256 NSString *kMediaKeyUsingBundleIdentifiersDefaultsKey = @"SPApplicationsNeedingMediaKeys";
257 NSString *kIgnoreMediaKeysDefaultsKey = @"SPIgnoreMediaKeys";
261 -(void)mediaKeyAppListChanged;
263 if([_mediaKeyAppList count] == 0) return;
267 for (NSValue *psnv in _mediaKeyAppList) {
268 ProcessSerialNumber psn; [psnv getValue:&psn];
269 NSDictionary *processInfo = [(id)ProcessInformationCopyDictionary(
271 kProcessDictionaryIncludeAllInformationMask
273 NSString *bundleIdentifier = [processInfo objectForKey:(id)kCFBundleIdentifierKey];
274 NSLog(@"%d: %@", i++, bundleIdentifier);
277 ProcessSerialNumber mySerial, topSerial;
278 GetCurrentProcess(&mySerial);
279 [[_mediaKeyAppList objectAtIndex:0] getValue:&topSerial];
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(
291 kProcessDictionaryIncludeAllInformationMask
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;
329 kEventParamProcessID,
330 typeProcessSerialNumber,
337 [self appTerminated:deadPSN];
338 return CallNextEventHandler(nextHandler, evt);