123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346 |
- // Copyright (c) 2010 Spotify AB
- #import "SPMediaKeyTap.h"
- #import "SPInvocationGrabbing/NSObject+SPInvocationGrabbing.h" // https://gist.github.com/511181, in submodule
- @interface SPMediaKeyTap ()
- -(BOOL)shouldInterceptMediaKeyEvents;
- -(void)setShouldInterceptMediaKeyEvents:(BOOL)newSetting;
- -(void)startWatchingAppSwitching;
- -(void)stopWatchingAppSwitching;
- -(void)eventTapThread;
- @end
- static SPMediaKeyTap *singleton = nil;
- static pascal OSStatus appSwitched (EventHandlerCallRef nextHandler, EventRef evt, void* userData);
- static pascal OSStatus appTerminated (EventHandlerCallRef nextHandler, EventRef evt, void* userData);
- static CGEventRef tapEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon);
- // Inspired by http://gist.github.com/546311
- @implementation SPMediaKeyTap
- #pragma mark -
- #pragma mark Setup and teardown
- -(id)initWithDelegate:(id)delegate;
- {
- _delegate = delegate;
- [self startWatchingAppSwitching];
- singleton = self;
- _mediaKeyAppList = [NSMutableArray new];
- _tapThreadRL=nil;
- _eventPort=nil;
- _eventPortSource=nil;
- return self;
- }
- -(void)dealloc;
- {
- [self stopWatchingMediaKeys];
- [self stopWatchingAppSwitching];
- [_mediaKeyAppList release];
- [super dealloc];
- }
- -(void)startWatchingAppSwitching;
- {
- // Listen to "app switched" event, so that we don't intercept media keys if we
- // weren't the last "media key listening" app to be active
- EventTypeSpec eventType = { kEventClassApplication, kEventAppFrontSwitched };
- OSStatus err = InstallApplicationEventHandler(NewEventHandlerUPP(appSwitched), 1, &eventType, self, &_app_switching_ref);
- assert(err == noErr);
-
- eventType.eventKind = kEventAppTerminated;
- err = InstallApplicationEventHandler(NewEventHandlerUPP(appTerminated), 1, &eventType, self, &_app_terminating_ref);
- assert(err == noErr);
- }
- -(void)stopWatchingAppSwitching;
- {
- if(!_app_switching_ref) return;
- RemoveEventHandler(_app_switching_ref);
- _app_switching_ref = NULL;
- }
- -(void)startWatchingMediaKeys;{
- // Prevent having multiple mediaKeys threads
- [self stopWatchingMediaKeys];
-
- [self setShouldInterceptMediaKeyEvents:YES];
-
- // Add an event tap to intercept the system defined media key events
- _eventPort = CGEventTapCreate(kCGSessionEventTap,
- kCGHeadInsertEventTap,
- kCGEventTapOptionDefault,
- CGEventMaskBit(NX_SYSDEFINED),
- tapEventCallback,
- self);
- assert(_eventPort != NULL);
-
- _eventPortSource = CFMachPortCreateRunLoopSource(kCFAllocatorSystemDefault, _eventPort, 0);
- assert(_eventPortSource != NULL);
-
- // Let's do this in a separate thread so that a slow app doesn't lag the event tap
- [NSThread detachNewThreadSelector:@selector(eventTapThread) toTarget:self withObject:nil];
- }
- -(void)stopWatchingMediaKeys;
- {
- // TODO<nevyn>: Shut down thread, remove event tap port and source
-
- if(_tapThreadRL){
- CFRunLoopStop(_tapThreadRL);
- _tapThreadRL=nil;
- }
-
- if(_eventPort){
- CFMachPortInvalidate(_eventPort);
- CFRelease(_eventPort);
- _eventPort=nil;
- }
-
- if(_eventPortSource){
- CFRelease(_eventPortSource);
- _eventPortSource=nil;
- }
- }
- #pragma mark -
- #pragma mark Accessors
- +(BOOL)usesGlobalMediaKeyTap
- {
- #ifdef _DEBUG
- // breaking in gdb with a key tap inserted sometimes locks up all mouse and keyboard input forever, forcing reboot
- return NO;
- #else
- // XXX(nevyn): MediaKey event tap doesn't work on 10.4, feel free to figure out why if you have the energy.
- return
- ![[NSUserDefaults standardUserDefaults] boolForKey:kIgnoreMediaKeysDefaultsKey]
- && floor(NSAppKitVersionNumber) >= 949/*NSAppKitVersionNumber10_5*/;
- #endif
- }
- + (NSArray*)defaultMediaKeyUserBundleIdentifiers;
- {
- return [NSArray arrayWithObjects:
- [[NSBundle mainBundle] bundleIdentifier], // your app
- @"com.spotify.client",
- @"com.apple.iTunes",
- @"com.apple.QuickTimePlayerX",
- @"com.apple.quicktimeplayer",
- @"com.apple.iWork.Keynote",
- @"com.apple.iPhoto",
- @"org.videolan.vlc",
- @"com.apple.Aperture",
- @"com.plexsquared.Plex",
- @"com.soundcloud.desktop",
- @"org.niltsh.MPlayerX",
- @"com.ilabs.PandorasHelper",
- @"com.mahasoftware.pandabar",
- @"com.bitcartel.pandorajam",
- @"org.clementine-player.clementine",
- @"fm.last.Last.fm",
- @"fm.last.Scrobbler",
- @"com.beatport.BeatportPro",
- @"com.Timenut.SongKey",
- @"com.macromedia.fireworks", // the tap messes up their mouse input
- @"at.justp.Theremin",
- @"ru.ya.themblsha.YandexMusic",
- @"com.jriver.MediaCenter18",
- @"com.jriver.MediaCenter19",
- @"com.jriver.MediaCenter20",
- @"co.rackit.mate",
- @"com.ttitt.b-music",
- @"com.beardedspice.BeardedSpice",
- @"com.plug.Plug",
- @"com.plug.Plug2",
- @"com.netease.163music",
- nil
- ];
- }
- -(BOOL)shouldInterceptMediaKeyEvents;
- {
- BOOL shouldIntercept = NO;
- @synchronized(self) {
- shouldIntercept = _shouldInterceptMediaKeyEvents;
- }
- return shouldIntercept;
- }
- -(void)pauseTapOnTapThread:(BOOL)yeahno;
- {
- CGEventTapEnable(self->_eventPort, yeahno);
- }
- -(void)setShouldInterceptMediaKeyEvents:(BOOL)newSetting;
- {
- BOOL oldSetting;
- @synchronized(self) {
- oldSetting = _shouldInterceptMediaKeyEvents;
- _shouldInterceptMediaKeyEvents = newSetting;
- }
- if(_tapThreadRL && oldSetting != newSetting) {
- id grab = [self grab];
- [grab pauseTapOnTapThread:newSetting];
- NSTimer *timer = [NSTimer timerWithTimeInterval:0 invocation:[grab invocation] repeats:NO];
- CFRunLoopAddTimer(_tapThreadRL, (CFRunLoopTimerRef)timer, kCFRunLoopCommonModes);
- }
- }
- #pragma mark
- #pragma mark -
- #pragma mark Event tap callbacks
- // Note: method called on background thread
- static CGEventRef tapEventCallback2(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
- {
- SPMediaKeyTap *self = refcon;
- if(type == kCGEventTapDisabledByTimeout) {
- NSLog(@"Media key event tap was disabled by timeout");
- CGEventTapEnable(self->_eventPort, TRUE);
- return event;
- } else if(type == kCGEventTapDisabledByUserInput) {
- // Was disabled manually by -[pauseTapOnTapThread]
- return event;
- }
- NSEvent *nsEvent = nil;
- @try {
- nsEvent = [NSEvent eventWithCGEvent:event];
- }
- @catch (NSException * e) {
- NSLog(@"Strange CGEventType: %d: %@", type, e);
- assert(0);
- return event;
- }
- if (type != NX_SYSDEFINED || [nsEvent subtype] != SPSystemDefinedEventMediaKeys)
- return event;
- int keyCode = (([nsEvent data1] & 0xFFFF0000) >> 16);
- if (keyCode != NX_KEYTYPE_PLAY && keyCode != NX_KEYTYPE_FAST && keyCode != NX_KEYTYPE_REWIND && keyCode != NX_KEYTYPE_PREVIOUS && keyCode != NX_KEYTYPE_NEXT)
- return event;
- if (![self shouldInterceptMediaKeyEvents])
- return event;
-
- [nsEvent retain]; // matched in handleAndReleaseMediaKeyEvent:
- [self performSelectorOnMainThread:@selector(handleAndReleaseMediaKeyEvent:) withObject:nsEvent waitUntilDone:NO];
-
- return NULL;
- }
- static CGEventRef tapEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
- {
- NSAutoreleasePool *pool = [NSAutoreleasePool new];
- CGEventRef ret = tapEventCallback2(proxy, type, event, refcon);
- [pool drain];
- return ret;
- }
- // event will have been retained in the other thread
- -(void)handleAndReleaseMediaKeyEvent:(NSEvent *)event {
- [event autorelease];
-
- [_delegate mediaKeyTap:self receivedMediaKeyEvent:event];
- }
- -(void)eventTapThread;
- {
- _tapThreadRL = CFRunLoopGetCurrent();
- CFRunLoopAddSource(_tapThreadRL, _eventPortSource, kCFRunLoopCommonModes);
- CFRunLoopRun();
- }
- #pragma mark Task switching callbacks
- NSString *kMediaKeyUsingBundleIdentifiersDefaultsKey = @"SPApplicationsNeedingMediaKeys";
- NSString *kIgnoreMediaKeysDefaultsKey = @"SPIgnoreMediaKeys";
- -(void)mediaKeyAppListChanged;
- {
- if([_mediaKeyAppList count] == 0) return;
-
- /*NSLog(@"--");
- int i = 0;
- for (NSValue *psnv in _mediaKeyAppList) {
- ProcessSerialNumber psn; [psnv getValue:&psn];
- NSDictionary *processInfo = [(id)ProcessInformationCopyDictionary(
- &psn,
- kProcessDictionaryIncludeAllInformationMask
- ) autorelease];
- NSString *bundleIdentifier = [processInfo objectForKey:(id)kCFBundleIdentifierKey];
- NSLog(@"%d: %@", i++, bundleIdentifier);
- }*/
-
- ProcessSerialNumber mySerial, topSerial;
- GetCurrentProcess(&mySerial);
- [[_mediaKeyAppList objectAtIndex:0] getValue:&topSerial];
- Boolean same;
- OSErr err = SameProcess(&mySerial, &topSerial, &same);
- [self setShouldInterceptMediaKeyEvents:(err == noErr && same)];
- }
- -(void)appIsNowFrontmost:(ProcessSerialNumber)psn;
- {
- NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)];
-
- NSDictionary *processInfo = [(id)ProcessInformationCopyDictionary(
- &psn,
- kProcessDictionaryIncludeAllInformationMask
- ) autorelease];
- NSString *bundleIdentifier = [processInfo objectForKey:(id)kCFBundleIdentifierKey];
- NSArray *whitelistIdentifiers = [[NSUserDefaults standardUserDefaults] arrayForKey:kMediaKeyUsingBundleIdentifiersDefaultsKey];
- if(![whitelistIdentifiers containsObject:bundleIdentifier]) return;
- [_mediaKeyAppList removeObject:psnv];
- [_mediaKeyAppList insertObject:psnv atIndex:0];
- [self mediaKeyAppListChanged];
- }
- -(void)appTerminated:(ProcessSerialNumber)psn;
- {
- NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)];
- [_mediaKeyAppList removeObject:psnv];
- [self mediaKeyAppListChanged];
- }
- static pascal OSStatus appSwitched (EventHandlerCallRef nextHandler, EventRef evt, void* userData)
- {
- SPMediaKeyTap *self = (id)userData;
- ProcessSerialNumber newSerial;
- GetFrontProcess(&newSerial);
-
- [self appIsNowFrontmost:newSerial];
-
- return CallNextEventHandler(nextHandler, evt);
- }
- static pascal OSStatus appTerminated (EventHandlerCallRef nextHandler, EventRef evt, void* userData)
- {
- SPMediaKeyTap *self = (id)userData;
-
- ProcessSerialNumber deadPSN;
- GetEventParameter(
- evt,
- kEventParamProcessID,
- typeProcessSerialNumber,
- NULL,
- sizeof(deadPSN),
- NULL,
- &deadPSN
- );
-
- [self appTerminated:deadPSN];
- return CallNextEventHandler(nextHandler, evt);
- }
- @end
|