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 [NSArray arrayWithObjects:
135 [[NSBundle mainBundle] bundleIdentifier], // your app
136 @"com.spotify.client",
138 @"com.apple.QuickTimePlayerX",
139 @"com.apple.quicktimeplayer",
140 @"com.apple.iWork.Keynote",
143 @"com.apple.Aperture",
144 @"com.plexsquared.Plex",
145 @"com.soundcloud.desktop",
146 @"org.niltsh.MPlayerX",
147 @"com.ilabs.PandorasHelper",
148 @"com.mahasoftware.pandabar",
149 @"com.bitcartel.pandorajam",
150 @"org.clementine-player.clementine",
152 @"com.beatport.BeatportPro",
153 @"com.Timenut.SongKey",
154 @"com.macromedia.fireworks", // the tap messes up their mouse input
160 -(BOOL)shouldInterceptMediaKeyEvents;
162 BOOL shouldIntercept = NO;
163 @synchronized(self) {
164 shouldIntercept = _shouldInterceptMediaKeyEvents;
166 return shouldIntercept;
169 -(void)pauseTapOnTapThread:(BOOL)yeahno;
171 CGEventTapEnable(self->_eventPort, yeahno);
173 -(void)setShouldInterceptMediaKeyEvents:(BOOL)newSetting;
176 @synchronized(self) {
177 oldSetting = _shouldInterceptMediaKeyEvents;
178 _shouldInterceptMediaKeyEvents = newSetting;
180 if(_tapThreadRL && oldSetting != newSetting) {
181 id grab = [self grab];
182 [grab pauseTapOnTapThread:newSetting];
183 NSTimer *timer = [NSTimer timerWithTimeInterval:0 invocation:[grab invocation] repeats:NO];
184 CFRunLoopAddTimer(_tapThreadRL, (CFRunLoopTimerRef)timer, kCFRunLoopCommonModes);
190 #pragma mark Event tap callbacks
192 // Note: method called on background thread
194 static CGEventRef tapEventCallback2(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
196 SPMediaKeyTap *self = refcon;
198 if(type == kCGEventTapDisabledByTimeout) {
199 NSLog(@"Media key event tap was disabled by timeout");
200 CGEventTapEnable(self->_eventPort, TRUE);
202 } else if(type == kCGEventTapDisabledByUserInput) {
203 // Was disabled manually by -[pauseTapOnTapThread]
206 NSEvent *nsEvent = nil;
208 nsEvent = [NSEvent eventWithCGEvent:event];
210 @catch (NSException * e) {
211 NSLog(@"Strange CGEventType: %d: %@", type, e);
216 if (type != NX_SYSDEFINED || [nsEvent subtype] != SPSystemDefinedEventMediaKeys)
219 int keyCode = (([nsEvent data1] & 0xFFFF0000) >> 16);
220 if (keyCode != NX_KEYTYPE_PLAY && keyCode != NX_KEYTYPE_FAST && keyCode != NX_KEYTYPE_REWIND && keyCode != NX_KEYTYPE_PREVIOUS && keyCode != NX_KEYTYPE_NEXT)
223 if (![self shouldInterceptMediaKeyEvents])
226 [nsEvent retain]; // matched in handleAndReleaseMediaKeyEvent:
227 [self performSelectorOnMainThread:@selector(handleAndReleaseMediaKeyEvent:) withObject:nsEvent waitUntilDone:NO];
232 static CGEventRef tapEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
234 NSAutoreleasePool *pool = [NSAutoreleasePool new];
235 CGEventRef ret = tapEventCallback2(proxy, type, event, refcon);
241 // event will have been retained in the other thread
242 -(void)handleAndReleaseMediaKeyEvent:(NSEvent *)event {
245 [_delegate mediaKeyTap:self receivedMediaKeyEvent:event];
249 -(void)eventTapThread;
251 _tapThreadRL = CFRunLoopGetCurrent();
252 CFRunLoopAddSource(_tapThreadRL, _eventPortSource, kCFRunLoopCommonModes);
256 #pragma mark Task switching callbacks
258 NSString *kMediaKeyUsingBundleIdentifiersDefaultsKey = @"SPApplicationsNeedingMediaKeys";
259 NSString *kIgnoreMediaKeysDefaultsKey = @"SPIgnoreMediaKeys";
263 -(void)mediaKeyAppListChanged;
265 if([_mediaKeyAppList count] == 0) return;
269 for (NSValue *psnv in _mediaKeyAppList) {
270 ProcessSerialNumber psn; [psnv getValue:&psn];
271 NSDictionary *processInfo = [(id)ProcessInformationCopyDictionary(
273 kProcessDictionaryIncludeAllInformationMask
275 NSString *bundleIdentifier = [processInfo objectForKey:(id)kCFBundleIdentifierKey];
276 NSLog(@"%d: %@", i++, bundleIdentifier);
279 ProcessSerialNumber mySerial, topSerial;
280 GetCurrentProcess(&mySerial);
281 [[_mediaKeyAppList objectAtIndex:0] getValue:&topSerial];
284 OSErr err = SameProcess(&mySerial, &topSerial, &same);
285 [self setShouldInterceptMediaKeyEvents:(err == noErr && same)];
287 -(void)appIsNowFrontmost:(ProcessSerialNumber)psn;
289 NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)];
291 NSDictionary *processInfo = [(id)ProcessInformationCopyDictionary(
293 kProcessDictionaryIncludeAllInformationMask
295 NSString *bundleIdentifier = [processInfo objectForKey:(id)kCFBundleIdentifierKey];
297 NSArray *whitelistIdentifiers = [[NSUserDefaults standardUserDefaults] arrayForKey:kMediaKeyUsingBundleIdentifiersDefaultsKey];
298 if(![whitelistIdentifiers containsObject:bundleIdentifier]) return;
300 [_mediaKeyAppList removeObject:psnv];
301 [_mediaKeyAppList insertObject:psnv atIndex:0];
302 [self mediaKeyAppListChanged];
304 -(void)appTerminated:(ProcessSerialNumber)psn;
306 NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)];
307 [_mediaKeyAppList removeObject:psnv];
308 [self mediaKeyAppListChanged];
311 static pascal OSStatus appSwitched (EventHandlerCallRef nextHandler, EventRef evt, void* userData)
313 SPMediaKeyTap *self = (id)userData;
315 ProcessSerialNumber newSerial;
316 GetFrontProcess(&newSerial);
318 [self appIsNowFrontmost:newSerial];
320 return CallNextEventHandler(nextHandler, evt);
323 static pascal OSStatus appTerminated (EventHandlerCallRef nextHandler, EventRef evt, void* userData)
325 SPMediaKeyTap *self = (id)userData;
327 ProcessSerialNumber deadPSN;
331 kEventParamProcessID,
332 typeProcessSerialNumber,
339 [self appTerminated:deadPSN];
340 return CallNextEventHandler(nextHandler, evt);