aboutsummaryrefslogtreecommitdiff
path: root/VKPC/SPMediaKeyTap.m
diff options
context:
space:
mode:
Diffstat (limited to 'VKPC/SPMediaKeyTap.m')
-rw-r--r--VKPC/SPMediaKeyTap.m374
1 files changed, 374 insertions, 0 deletions
diff --git a/VKPC/SPMediaKeyTap.m b/VKPC/SPMediaKeyTap.m
new file mode 100644
index 0000000..4112b78
--- /dev/null
+++ b/VKPC/SPMediaKeyTap.m
@@ -0,0 +1,374 @@
+// 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 BOOL inited = NO;
+
+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);
+
+static NSArray *defaultMediaKeyUserBundleIdentifiers;
+
+// Inspired by http://gist.github.com/546311
+
+@implementation SPMediaKeyTap
+
+#pragma mark -
+#pragma mark Setup and teardown
+
+- (id)initWithDelegate:(id)delegate {
+ [SPMediaKeyTap initialize];
+ _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, (__bridge void *)self, &_app_switching_ref);
+ assert(err == noErr);
+
+ eventType.eventKind = kEventAppTerminated;
+ err = InstallApplicationEventHandler(NewEventHandlerUPP(appTerminated), 1, &eventType, (__bridge void *)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,
+ (__bridge void *)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 {
+ return [[NSUserDefaults standardUserDefaults] boolForKey:VKPCPreferencesCatchMediaButtons]
+ && floor(NSAppKitVersionNumber) >= NSAppKitVersionNumber10_5;
+}
+
++ (void)initialize {
+ if (inited)
+ return;
+
+ defaultMediaKeyUserBundleIdentifiers = [NSArray arrayWithObjects:
+ [[NSBundle mainBundle] bundleIdentifier], // your app
+ @"com.apple.iTunes",
+ @"org.videolan.vlc",
+ // @"com.spotify.client",
+ @"com.apple.QuickTimePlayerX",
+ @"com.apple.quicktimeplayer",
+ // @"com.apple.iWork.Keynote",
+ // @"com.apple.iPhoto",
+ // @"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",
+ nil
+ ];
+// [defaultMediaKeyUserBundleIdentifiers retain];
+ inited = YES;
+}
++ (NSArray *)defaultMediaKeyUserBundleIdentifiers {
+ return defaultMediaKeyUserBundleIdentifiers;
+}
+
+
+- (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, (__bridge 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) {
+// NSLog(@"tapEventCallback2() 1");
+ SPMediaKeyTap *self = (__bridge id)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;
+ }
+
+ int keyCode = (([nsEvent data1] & 0xFFFF0000) >> 16);
+
+#ifdef DEBUG
+ int keyFlags = ([nsEvent data1] & 0x0000FFFF);
+ NSLog(@"Event: e.type=%lu, e.subtype=%d, e.keyCode=%d, e.keyFlags=%d, e.data1=%ld",
+ nsEvent.type, nsEvent.subtype, keyCode, keyFlags, nsEvent.data1);
+#endif
+
+ if (type != NX_SYSDEFINED || [nsEvent subtype] != SPSystemDefinedEventMediaKeys)
+ return event;
+
+ 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) {
+ CGEventRef ret;
+ @autoreleasepool {
+ ret = tapEventCallback2(proxy, type, event, refcon);
+ }
+ return ret;
+}
+
+
+// event will have been retained in the other thread
+- (void)handleAndReleaseMediaKeyEvent:(NSEvent *)event {
+ [_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);
+// }
+// NSLog(@"--");
+
+ 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 {
+// NSLog(@"- appIsNowFrontmost: 1");
+ NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)];
+ NSDictionary *processInfo = (__bridge id)ProcessInformationCopyDictionary(
+ &psn,
+ kProcessDictionaryIncludeAllInformationMask
+ );
+// NSLog(@"- appisnowfrontmost; processInfo: %@", processInfo);
+ NSString *bundleIdentifier = [processInfo objectForKey:(id)kCFBundleIdentifierKey];
+
+ // NSArray *whitelistIdentifiers = [[NSUserDefaults standardUserDefaults] arrayForKey:kMediaKeyUsingBundleIdentifiersDefaultsKey];
+ if (![defaultMediaKeyUserBundleIdentifiers containsObject:bundleIdentifier]) {
+ if ([_mediaKeyAppList count] > 0) {
+ NSValue *tmpPsvn = [_mediaKeyAppList objectAtIndex:0];
+ ProcessSerialNumber tmpPsn;
+ [tmpPsvn getValue:&tmpPsn];
+
+ NSDictionary *tmpProcessInfo = (__bridge id)ProcessInformationCopyDictionary(
+ &tmpPsn,
+ kProcessDictionaryIncludeAllInformationMask
+ );
+ pid_t tmpPid = (pid_t)[(NSNumber *)[tmpProcessInfo objectForKey:(id)@"pid"] integerValue];
+ if (tmpPid == VKPCPID) {
+ return;
+ }
+ }
+
+ ProcessSerialNumber thisPsn;
+ GetProcessForPID(VKPCPID, &thisPsn);
+
+ NSValue *thisPsnv = [NSValue valueWithBytes:&thisPsn objCType:@encode(ProcessSerialNumber)];
+
+ [_mediaKeyAppList removeObject:psnv];
+ [_mediaKeyAppList removeObject:thisPsnv];
+ [_mediaKeyAppList insertObject:thisPsnv atIndex:0];
+
+ [self mediaKeyAppListChanged];
+ return;
+ }
+
+ [_mediaKeyAppList removeObject:psnv];
+ [_mediaKeyAppList insertObject:psnv atIndex:0];
+ [self mediaKeyAppListChanged];
+
+// NSLog(@"- appIsNowFrontmost: 2");
+}
+
+- (void)appTerminated:(ProcessSerialNumber)psn {
+// NSLog(@"- appterminated");
+ NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)];
+ [_mediaKeyAppList removeObject:psnv];
+ [self mediaKeyAppListChanged];
+}
+
+static pascal OSStatus appSwitched (EventHandlerCallRef nextHandler, EventRef evt, void* userData) {
+// NSLog(@"appswitched");
+ SPMediaKeyTap *self = (__bridge id)userData;
+
+ ProcessSerialNumber newSerial;
+ GetFrontProcess(&newSerial);
+
+ [self appIsNowFrontmost:newSerial];
+
+ return CallNextEventHandler(nextHandler, evt);
+}
+
+static pascal OSStatus appTerminated (EventHandlerCallRef nextHandler, EventRef evt, void* userData) {
+// NSLog(@"appTermminated");
+ SPMediaKeyTap *self = (__bridge id)userData;
+
+ ProcessSerialNumber deadPSN;
+
+ GetEventParameter(
+ evt,
+ kEventParamProcessID,
+ typeProcessSerialNumber,
+ NULL,
+ sizeof(deadPSN),
+ NULL,
+ &deadPSN
+ );
+
+
+ [self appTerminated:deadPSN];
+ return CallNextEventHandler(nextHandler, evt);
+}
+
+@end \ No newline at end of file