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 @"fm.last.Scrobbler",
153 @"com.beatport.BeatportPro",
154 @"com.Timenut.SongKey",
155 @"com.macromedia.fireworks", // the tap messes up their mouse input
156 @"at.justp.Theremin",
157 @"ru.ya.themblsha.YandexMusic",
158 @"com.jriver.MediaCenter18",
159 @"com.jriver.MediaCenter19",
160 @"com.jriver.MediaCenter20",
166 -(BOOL)shouldInterceptMediaKeyEvents;
168 BOOL shouldIntercept = NO;
169 @synchronized(self) {
170 shouldIntercept = _shouldInterceptMediaKeyEvents;
172 return shouldIntercept;
175 -(void)pauseTapOnTapThread:(BOOL)yeahno;
177 CGEventTapEnable(self->_eventPort, yeahno);
179 -(void)setShouldInterceptMediaKeyEvents:(BOOL)newSetting;
182 @synchronized(self) {
183 oldSetting = _shouldInterceptMediaKeyEvents;
184 _shouldInterceptMediaKeyEvents = newSetting;
186 if(_tapThreadRL && oldSetting != newSetting) {
187 id grab = [self grab];
188 [grab pauseTapOnTapThread:newSetting];
189 NSTimer *timer = [NSTimer timerWithTimeInterval:0 invocation:[grab invocation] repeats:NO];
190 CFRunLoopAddTimer(_tapThreadRL, (CFRunLoopTimerRef)timer, kCFRunLoopCommonModes);
196 #pragma mark Event tap callbacks
198 // Note: method called on background thread
200 static CGEventRef tapEventCallback2(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
202 SPMediaKeyTap *self = refcon;
204 if(type == kCGEventTapDisabledByTimeout) {
205 NSLog(@"Media key event tap was disabled by timeout");
206 CGEventTapEnable(self->_eventPort, TRUE);
208 } else if(type == kCGEventTapDisabledByUserInput) {
209 // Was disabled manually by -[pauseTapOnTapThread]
212 NSEvent *nsEvent = nil;
214 nsEvent = [NSEvent eventWithCGEvent:event];
216 @catch (NSException * e) {
217 NSLog(@"Strange CGEventType: %d: %@", type, e);
222 if (type != NX_SYSDEFINED || [nsEvent subtype] != SPSystemDefinedEventMediaKeys)
225 int keyCode = (([nsEvent data1] & 0xFFFF0000) >> 16);
226 if (keyCode != NX_KEYTYPE_PLAY && keyCode != NX_KEYTYPE_FAST && keyCode != NX_KEYTYPE_REWIND && keyCode != NX_KEYTYPE_PREVIOUS && keyCode != NX_KEYTYPE_NEXT)
229 if (![self shouldInterceptMediaKeyEvents])
232 [nsEvent retain]; // matched in handleAndReleaseMediaKeyEvent:
233 [self performSelectorOnMainThread:@selector(handleAndReleaseMediaKeyEvent:) withObject:nsEvent waitUntilDone:NO];
238 static CGEventRef tapEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
240 NSAutoreleasePool *pool = [NSAutoreleasePool new];
241 CGEventRef ret = tapEventCallback2(proxy, type, event, refcon);
247 // event will have been retained in the other thread
248 -(void)handleAndReleaseMediaKeyEvent:(NSEvent *)event {
251 [_delegate mediaKeyTap:self receivedMediaKeyEvent:event];
255 -(void)eventTapThread;
257 _tapThreadRL = CFRunLoopGetCurrent();
258 CFRunLoopAddSource(_tapThreadRL, _eventPortSource, kCFRunLoopCommonModes);
262 #pragma mark Task switching callbacks
264 NSString *kMediaKeyUsingBundleIdentifiersDefaultsKey = @"SPApplicationsNeedingMediaKeys";
265 NSString *kIgnoreMediaKeysDefaultsKey = @"SPIgnoreMediaKeys";
269 -(void)mediaKeyAppListChanged;
271 if([_mediaKeyAppList count] == 0) return;
275 for (NSValue *psnv in _mediaKeyAppList) {
276 ProcessSerialNumber psn; [psnv getValue:&psn];
277 NSDictionary *processInfo = [(id)ProcessInformationCopyDictionary(
279 kProcessDictionaryIncludeAllInformationMask
281 NSString *bundleIdentifier = [processInfo objectForKey:(id)kCFBundleIdentifierKey];
282 NSLog(@"%d: %@", i++, bundleIdentifier);
285 ProcessSerialNumber mySerial, topSerial;
286 GetCurrentProcess(&mySerial);
287 [[_mediaKeyAppList objectAtIndex:0] getValue:&topSerial];
290 OSErr err = SameProcess(&mySerial, &topSerial, &same);
291 [self setShouldInterceptMediaKeyEvents:(err == noErr && same)];
293 -(void)appIsNowFrontmost:(ProcessSerialNumber)psn;
295 NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)];
297 NSDictionary *processInfo = [(id)ProcessInformationCopyDictionary(
299 kProcessDictionaryIncludeAllInformationMask
301 NSString *bundleIdentifier = [processInfo objectForKey:(id)kCFBundleIdentifierKey];
303 NSArray *whitelistIdentifiers = [[NSUserDefaults standardUserDefaults] arrayForKey:kMediaKeyUsingBundleIdentifiersDefaultsKey];
304 if(![whitelistIdentifiers containsObject:bundleIdentifier]) return;
306 [_mediaKeyAppList removeObject:psnv];
307 [_mediaKeyAppList insertObject:psnv atIndex:0];
308 [self mediaKeyAppListChanged];
310 -(void)appTerminated:(ProcessSerialNumber)psn;
312 NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)];
313 [_mediaKeyAppList removeObject:psnv];
314 [self mediaKeyAppListChanged];
317 static pascal OSStatus appSwitched (EventHandlerCallRef nextHandler, EventRef evt, void* userData)
319 SPMediaKeyTap *self = (id)userData;
321 ProcessSerialNumber newSerial;
322 GetFrontProcess(&newSerial);
324 [self appIsNowFrontmost:newSerial];
326 return CallNextEventHandler(nextHandler, evt);
329 static pascal OSStatus appTerminated (EventHandlerCallRef nextHandler, EventRef evt, void* userData)
331 SPMediaKeyTap *self = (id)userData;
333 ProcessSerialNumber deadPSN;
337 kEventParamProcessID,
338 typeProcessSerialNumber,
345 [self appTerminated:deadPSN];
346 return CallNextEventHandler(nextHandler, evt);