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",
162 @"com.ttitt.b-music",
163 @"com.beardedspice.BeardedSpice",
165 @"com.netease.163music",
171 -(BOOL)shouldInterceptMediaKeyEvents;
173 BOOL shouldIntercept = NO;
174 @synchronized(self) {
175 shouldIntercept = _shouldInterceptMediaKeyEvents;
177 return shouldIntercept;
180 -(void)pauseTapOnTapThread:(BOOL)yeahno;
182 CGEventTapEnable(self->_eventPort, yeahno);
184 -(void)setShouldInterceptMediaKeyEvents:(BOOL)newSetting;
187 @synchronized(self) {
188 oldSetting = _shouldInterceptMediaKeyEvents;
189 _shouldInterceptMediaKeyEvents = newSetting;
191 if(_tapThreadRL && oldSetting != newSetting) {
192 id grab = [self grab];
193 [grab pauseTapOnTapThread:newSetting];
194 NSTimer *timer = [NSTimer timerWithTimeInterval:0 invocation:[grab invocation] repeats:NO];
195 CFRunLoopAddTimer(_tapThreadRL, (CFRunLoopTimerRef)timer, kCFRunLoopCommonModes);
201 #pragma mark Event tap callbacks
203 // Note: method called on background thread
205 static CGEventRef tapEventCallback2(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
207 SPMediaKeyTap *self = refcon;
209 if(type == kCGEventTapDisabledByTimeout) {
210 NSLog(@"Media key event tap was disabled by timeout");
211 CGEventTapEnable(self->_eventPort, TRUE);
213 } else if(type == kCGEventTapDisabledByUserInput) {
214 // Was disabled manually by -[pauseTapOnTapThread]
217 NSEvent *nsEvent = nil;
219 nsEvent = [NSEvent eventWithCGEvent:event];
221 @catch (NSException * e) {
222 NSLog(@"Strange CGEventType: %d: %@", type, e);
227 if (type != NX_SYSDEFINED || [nsEvent subtype] != SPSystemDefinedEventMediaKeys)
230 int keyCode = (([nsEvent data1] & 0xFFFF0000) >> 16);
231 if (keyCode != NX_KEYTYPE_PLAY && keyCode != NX_KEYTYPE_FAST && keyCode != NX_KEYTYPE_REWIND && keyCode != NX_KEYTYPE_PREVIOUS && keyCode != NX_KEYTYPE_NEXT)
234 if (![self shouldInterceptMediaKeyEvents])
237 [nsEvent retain]; // matched in handleAndReleaseMediaKeyEvent:
238 [self performSelectorOnMainThread:@selector(handleAndReleaseMediaKeyEvent:) withObject:nsEvent waitUntilDone:NO];
243 static CGEventRef tapEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
245 NSAutoreleasePool *pool = [NSAutoreleasePool new];
246 CGEventRef ret = tapEventCallback2(proxy, type, event, refcon);
252 // event will have been retained in the other thread
253 -(void)handleAndReleaseMediaKeyEvent:(NSEvent *)event {
256 [_delegate mediaKeyTap:self receivedMediaKeyEvent:event];
260 -(void)eventTapThread;
262 _tapThreadRL = CFRunLoopGetCurrent();
263 CFRunLoopAddSource(_tapThreadRL, _eventPortSource, kCFRunLoopCommonModes);
267 #pragma mark Task switching callbacks
269 NSString *kMediaKeyUsingBundleIdentifiersDefaultsKey = @"SPApplicationsNeedingMediaKeys";
270 NSString *kIgnoreMediaKeysDefaultsKey = @"SPIgnoreMediaKeys";
274 -(void)mediaKeyAppListChanged;
276 if([_mediaKeyAppList count] == 0) return;
280 for (NSValue *psnv in _mediaKeyAppList) {
281 ProcessSerialNumber psn; [psnv getValue:&psn];
282 NSDictionary *processInfo = [(id)ProcessInformationCopyDictionary(
284 kProcessDictionaryIncludeAllInformationMask
286 NSString *bundleIdentifier = [processInfo objectForKey:(id)kCFBundleIdentifierKey];
287 NSLog(@"%d: %@", i++, bundleIdentifier);
290 ProcessSerialNumber mySerial, topSerial;
291 GetCurrentProcess(&mySerial);
292 [[_mediaKeyAppList objectAtIndex:0] getValue:&topSerial];
295 OSErr err = SameProcess(&mySerial, &topSerial, &same);
296 [self setShouldInterceptMediaKeyEvents:(err == noErr && same)];
298 -(void)appIsNowFrontmost:(ProcessSerialNumber)psn;
300 NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)];
302 NSDictionary *processInfo = [(id)ProcessInformationCopyDictionary(
304 kProcessDictionaryIncludeAllInformationMask
306 NSString *bundleIdentifier = [processInfo objectForKey:(id)kCFBundleIdentifierKey];
308 NSArray *whitelistIdentifiers = [[NSUserDefaults standardUserDefaults] arrayForKey:kMediaKeyUsingBundleIdentifiersDefaultsKey];
309 if(![whitelistIdentifiers containsObject:bundleIdentifier]) return;
311 [_mediaKeyAppList removeObject:psnv];
312 [_mediaKeyAppList insertObject:psnv atIndex:0];
313 [self mediaKeyAppListChanged];
315 -(void)appTerminated:(ProcessSerialNumber)psn;
317 NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)];
318 [_mediaKeyAppList removeObject:psnv];
319 [self mediaKeyAppListChanged];
322 static pascal OSStatus appSwitched (EventHandlerCallRef nextHandler, EventRef evt, void* userData)
324 SPMediaKeyTap *self = (id)userData;
326 ProcessSerialNumber newSerial;
327 GetFrontProcess(&newSerial);
329 [self appIsNowFrontmost:newSerial];
331 return CallNextEventHandler(nextHandler, evt);
334 static pascal OSStatus appTerminated (EventHandlerCallRef nextHandler, EventRef evt, void* userData)
336 SPMediaKeyTap *self = (id)userData;
338 ProcessSerialNumber deadPSN;
342 kEventParamProcessID,
343 typeProcessSerialNumber,
350 [self appTerminated:deadPSN];
351 return CallNextEventHandler(nextHandler, evt);