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);
223 vlc_assert_unreachable();
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];
259 -(void)eventTapThread;
261 _tapThreadRL = CFRunLoopGetCurrent();
262 CFRunLoopAddSource(_tapThreadRL, _eventPortSource, kCFRunLoopCommonModes);
266 #pragma mark Task switching callbacks
268 NSString *kMediaKeyUsingBundleIdentifiersDefaultsKey = @"SPApplicationsNeedingMediaKeys";
269 NSString *kIgnoreMediaKeysDefaultsKey = @"SPIgnoreMediaKeys";
273 -(void)mediaKeyAppListChanged;
275 if([_mediaKeyAppList count] == 0) return;
279 for (NSValue *psnv in _mediaKeyAppList) {
280 ProcessSerialNumber psn; [psnv getValue:&psn];
281 NSDictionary *processInfo = [(id)ProcessInformationCopyDictionary(
283 kProcessDictionaryIncludeAllInformationMask
285 NSString *bundleIdentifier = [processInfo objectForKey:(id)kCFBundleIdentifierKey];
286 NSLog(@"%d: %@", i++, bundleIdentifier);
289 ProcessSerialNumber mySerial, topSerial;
290 GetCurrentProcess(&mySerial);
291 [[_mediaKeyAppList objectAtIndex:0] getValue:&topSerial];
294 OSErr err = SameProcess(&mySerial, &topSerial, &same);
295 [self setShouldInterceptMediaKeyEvents:(err == noErr && same)];
297 -(void)appIsNowFrontmost:(ProcessSerialNumber)psn;
299 NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)];
301 NSDictionary *processInfo = [(id)ProcessInformationCopyDictionary(
303 kProcessDictionaryIncludeAllInformationMask
305 NSString *bundleIdentifier = [processInfo objectForKey:(id)kCFBundleIdentifierKey];
307 NSArray *whitelistIdentifiers = [[NSUserDefaults standardUserDefaults] arrayForKey:kMediaKeyUsingBundleIdentifiersDefaultsKey];
308 if(![whitelistIdentifiers containsObject:bundleIdentifier]) return;
310 [_mediaKeyAppList removeObject:psnv];
311 [_mediaKeyAppList insertObject:psnv atIndex:0];
312 [self mediaKeyAppListChanged];
314 -(void)appTerminated:(ProcessSerialNumber)psn;
316 NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)];
317 [_mediaKeyAppList removeObject:psnv];
318 [self mediaKeyAppListChanged];
321 static pascal OSStatus appSwitched (EventHandlerCallRef nextHandler, EventRef evt, void* userData)
323 SPMediaKeyTap *self = (id)userData;
325 ProcessSerialNumber newSerial;
326 GetFrontProcess(&newSerial);
328 [self appIsNowFrontmost:newSerial];
330 return CallNextEventHandler(nextHandler, evt);
333 static pascal OSStatus appTerminated (EventHandlerCallRef nextHandler, EventRef evt, void* userData)
335 SPMediaKeyTap *self = (id)userData;
337 ProcessSerialNumber deadPSN;
341 kEventParamProcessID,
342 typeProcessSerialNumber,
349 [self appTerminated:deadPSN];
350 return CallNextEventHandler(nextHandler, evt);