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" // https://gist.github.com/511181
16 @interface SPMediaKeyTap ()
17 -(BOOL)shouldInterceptMediaKeyEvents;
18 -(void)startWatchingAppSwitching;
19 -(void)stopWatchingAppSwitching;
20 -(void)eventTapThread;
22 static SPMediaKeyTap *singleton = nil;
24 static pascal OSStatus appSwitched (EventHandlerCallRef nextHandler, EventRef evt, void* userData);
25 static pascal OSStatus appTerminated (EventHandlerCallRef nextHandler, EventRef evt, void* userData);
26 static CGEventRef tapEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon);
29 // Inspired by http://gist.github.com/546311
31 @implementation SPMediaKeyTap
34 #pragma mark Setup and teardown
35 -(id)initWithDelegate:(id)delegate;
38 [self startWatchingAppSwitching];
40 _mediaKeyAppList = [NSMutableArray new];
45 [self stopWatchingMediaKeys];
46 [self stopWatchingAppSwitching];
47 [_mediaKeyAppList release];
51 -(void)startWatchingAppSwitching;
53 // Listen to "app switched" event, so that we don't intercept media keys if we
54 // weren't the last "media key listening" app to be active
55 EventTypeSpec eventType = { kEventClassApplication, kEventAppFrontSwitched };
56 OSStatus err = InstallApplicationEventHandler(NewEventHandlerUPP(appSwitched), 1, &eventType, self, &_app_switching_ref);
59 eventType.eventKind = kEventAppTerminated;
60 err = InstallApplicationEventHandler(NewEventHandlerUPP(appTerminated), 1, &eventType, self, &_app_terminating_ref);
63 -(void)stopWatchingAppSwitching;
65 if(!_app_switching_ref) return;
66 RemoveEventHandler(_app_switching_ref);
67 _app_switching_ref = NULL;
70 -(void)startWatchingMediaKeys;{
71 [self setShouldInterceptMediaKeyEvents:YES];
73 // Add an event tap to intercept the system defined media key events
74 _eventPort = CGEventTapCreate(kCGSessionEventTap,
75 kCGHeadInsertEventTap,
76 kCGEventTapOptionDefault,
77 CGEventMaskBit(NX_SYSDEFINED),
80 assert(_eventPort != NULL);
82 _eventPortSource = CFMachPortCreateRunLoopSource(kCFAllocatorSystemDefault, _eventPort, 0);
83 assert(_eventPortSource != NULL);
85 // Let's do this in a separate thread so that a slow app doesn't lag the event tap
86 [NSThread detachNewThreadSelector:@selector(eventTapThread) toTarget:self withObject:nil];
88 -(void)stopWatchingMediaKeys;
90 // TODO<nevyn>: Shut down thread, remove event tap port and source
94 #pragma mark Accessors
96 +(BOOL)usesGlobalMediaKeyTap
102 // XXX(nevyn): MediaKey event tap doesn't work on 10.4, feel free to figure out why if you have the energy.
103 return floor(NSAppKitVersionNumber) >= 949/*NSAppKitVersionNumber10_5*/;
107 + (NSArray*)defaultMediaKeyUserBundleIdentifiers;
109 return [NSArray arrayWithObjects:
110 @"com.spotify.client",
112 @"com.apple.QuickTimePlayerX",
113 @"com.apple.quicktimeplayer",
114 @"com.apple.iWork.Keynote",
117 @"com.apple.Aperture",
118 @"com.plexsquared.Plex",
119 @"com.soundcloud.desktop",
120 @"com.macromedia.fireworks", // the tap messes up their mouse input
126 -(BOOL)shouldInterceptMediaKeyEvents;
128 BOOL shouldIntercept = NO;
129 @synchronized(self) {
130 shouldIntercept = _shouldInterceptMediaKeyEvents;
132 return shouldIntercept;
135 -(void)pauseTapOnTapThread:(BOOL)yeahno;
137 CGEventTapEnable(self->_eventPort, yeahno);
139 -(void)setShouldInterceptMediaKeyEvents:(BOOL)newSetting;
142 @synchronized(self) {
143 oldSetting = _shouldInterceptMediaKeyEvents;
144 _shouldInterceptMediaKeyEvents = newSetting;
146 if(_tapThreadRL && oldSetting != newSetting) {
147 id grab = [self grab];
148 [grab pauseTapOnTapThread:newSetting];
149 NSTimer *timer = [NSTimer timerWithTimeInterval:0 invocation:[grab invocation] repeats:NO];
150 CFRunLoopAddTimer(_tapThreadRL, (CFRunLoopTimerRef)timer, kCFRunLoopCommonModes);
156 #pragma mark Event tap callbacks
158 // Note: method called on background thread
160 static CGEventRef tapEventCallback2(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
162 SPMediaKeyTap *self = refcon;
164 if(type == kCGEventTapDisabledByTimeout) {
165 NSLog(@"Media key event tap was disabled by timeout");
166 CGEventTapEnable(self->_eventPort, TRUE);
168 } else if(type == kCGEventTapDisabledByUserInput) {
169 // Was disabled manually by -[pauseTapOnTapThread]
172 NSEvent *nsEvent = nil;
174 nsEvent = [NSEvent eventWithCGEvent:event];
176 @catch (NSException * e) {
177 NSLog(@"Strange CGEventType: %d: %@", type, e);
182 if (type != NX_SYSDEFINED || [nsEvent subtype] != SPSystemDefinedEventMediaKeys)
185 int keyCode = (([nsEvent data1] & 0xFFFF0000) >> 16);
186 if (keyCode != NX_KEYTYPE_PLAY && keyCode != NX_KEYTYPE_FAST && keyCode != NX_KEYTYPE_REWIND)
189 if (![self shouldInterceptMediaKeyEvents])
192 [nsEvent retain]; // matched in handleAndReleaseMediaKeyEvent:
193 [self performSelectorOnMainThread:@selector(handleAndReleaseMediaKeyEvent:) withObject:nsEvent waitUntilDone:NO];
198 static CGEventRef tapEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
200 NSAutoreleasePool *pool = [NSAutoreleasePool new];
201 CGEventRef ret = tapEventCallback2(proxy, type, event, refcon);
207 // event will have been retained in the other thread
208 -(void)handleAndReleaseMediaKeyEvent:(NSEvent *)event {
211 [_delegate mediaKeyTap:self receivedMediaKeyEvent:event];
215 -(void)eventTapThread;
217 _tapThreadRL = CFRunLoopGetCurrent();
218 CFRunLoopAddSource(_tapThreadRL, _eventPortSource, kCFRunLoopCommonModes);
222 #pragma mark Task switching callbacks
224 NSString *kMediaKeyUsingBundleIdentifiersDefaultsKey = @"SPApplicationsNeedingMediaKeys";
227 -(void)mediaKeyAppListChanged;
229 if([_mediaKeyAppList count] == 0) return;
233 for (NSValue *psnv in _mediaKeyAppList) {
234 ProcessSerialNumber psn; [psnv getValue:&psn];
235 NSDictionary *processInfo = [(id)ProcessInformationCopyDictionary(
237 kProcessDictionaryIncludeAllInformationMask
239 NSString *bundleIdentifier = [processInfo objectForKey:(id)kCFBundleIdentifierKey];
240 NSLog(@"%d: %@", i++, bundleIdentifier);
243 ProcessSerialNumber mySerial, topSerial;
244 GetCurrentProcess(&mySerial);
245 [[_mediaKeyAppList objectAtIndex:0] getValue:&topSerial];
248 OSErr err = SameProcess(&mySerial, &topSerial, &same);
249 [self setShouldInterceptMediaKeyEvents:(err == noErr && same)];
252 -(void)appIsNowFrontmost:(ProcessSerialNumber)psn;
254 NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)];
256 NSDictionary *processInfo = [(id)ProcessInformationCopyDictionary(
258 kProcessDictionaryIncludeAllInformationMask
260 NSString *bundleIdentifier = [processInfo objectForKey:(id)kCFBundleIdentifierKey];
262 NSArray *whitelistIdentifiers = [[NSUserDefaults standardUserDefaults] arrayForKey:kMediaKeyUsingBundleIdentifiersDefaultsKey];
263 if(![whitelistIdentifiers containsObject:bundleIdentifier]) return;
265 [_mediaKeyAppList removeObject:psnv];
266 [_mediaKeyAppList insertObject:psnv atIndex:0];
267 [self mediaKeyAppListChanged];
269 -(void)appTerminated:(ProcessSerialNumber)psn;
271 NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)];
272 [_mediaKeyAppList removeObject:psnv];
273 [self mediaKeyAppListChanged];
276 static pascal OSStatus appSwitched (EventHandlerCallRef nextHandler, EventRef evt, void* userData)
278 SPMediaKeyTap *self = (id)userData;
280 ProcessSerialNumber newSerial;
281 GetFrontProcess(&newSerial);
283 [self appIsNowFrontmost:newSerial];
285 return CallNextEventHandler(nextHandler, evt);
288 static pascal OSStatus appTerminated (EventHandlerCallRef nextHandler, EventRef evt, void* userData)
290 SPMediaKeyTap *self = (id)userData;
292 ProcessSerialNumber deadPSN;
296 kEventParamProcessID,
297 typeProcessSerialNumber,
305 [self appTerminated:deadPSN];
306 return CallNextEventHandler(nextHandler, evt);