/thirdparty/SPMediaKeyTap/SPMediaKeyTap.m

http://github.com/tomahawk-player/tomahawk · Objective C · 334 lines · 251 code · 59 blank · 24 comment · 33 complexity · dd4f9d53718c9e8818f888ff66258303 MD5 · raw file

  1. // Copyright (c) 2010 Spotify AB
  2. #import "SPMediaKeyTap.h"
  3. #import "SPInvocationGrabbing/NSObject+SPInvocationGrabbing.h" // https://gist.github.com/511181, in submodule
  4. @interface SPMediaKeyTap ()
  5. -(BOOL)shouldInterceptMediaKeyEvents;
  6. -(void)setShouldInterceptMediaKeyEvents:(BOOL)newSetting;
  7. -(void)startWatchingAppSwitching;
  8. -(void)stopWatchingAppSwitching;
  9. -(void)eventTapThread;
  10. @end
  11. static SPMediaKeyTap *singleton = nil;
  12. static pascal OSStatus appSwitched (EventHandlerCallRef nextHandler, EventRef evt, void* userData);
  13. static pascal OSStatus appTerminated (EventHandlerCallRef nextHandler, EventRef evt, void* userData);
  14. static CGEventRef tapEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon);
  15. // Inspired by http://gist.github.com/546311
  16. @implementation SPMediaKeyTap
  17. #pragma mark -
  18. #pragma mark Setup and teardown
  19. -(id)initWithDelegate:(id)delegate;
  20. {
  21. _delegate = delegate;
  22. [self startWatchingAppSwitching];
  23. singleton = self;
  24. _mediaKeyAppList = [NSMutableArray new];
  25. _tapThreadRL=nil;
  26. _eventPort=nil;
  27. _eventPortSource=nil;
  28. return self;
  29. }
  30. -(void)dealloc;
  31. {
  32. [self stopWatchingMediaKeys];
  33. [self stopWatchingAppSwitching];
  34. [_mediaKeyAppList release];
  35. [super dealloc];
  36. }
  37. -(void)startWatchingAppSwitching;
  38. {
  39. // Listen to "app switched" event, so that we don't intercept media keys if we
  40. // weren't the last "media key listening" app to be active
  41. EventTypeSpec eventType = { kEventClassApplication, kEventAppFrontSwitched };
  42. OSStatus err = InstallApplicationEventHandler(NewEventHandlerUPP(appSwitched), 1, &eventType, self, &_app_switching_ref);
  43. assert(err == noErr);
  44. eventType.eventKind = kEventAppTerminated;
  45. err = InstallApplicationEventHandler(NewEventHandlerUPP(appTerminated), 1, &eventType, self, &_app_terminating_ref);
  46. assert(err == noErr);
  47. }
  48. -(void)stopWatchingAppSwitching;
  49. {
  50. if(!_app_switching_ref) return;
  51. RemoveEventHandler(_app_switching_ref);
  52. _app_switching_ref = NULL;
  53. }
  54. -(void)startWatchingMediaKeys;{
  55. // Prevent having multiple mediaKeys threads
  56. [self stopWatchingMediaKeys];
  57. [self setShouldInterceptMediaKeyEvents:YES];
  58. // Add an event tap to intercept the system defined media key events
  59. _eventPort = CGEventTapCreate(kCGSessionEventTap,
  60. kCGHeadInsertEventTap,
  61. kCGEventTapOptionDefault,
  62. CGEventMaskBit(NX_SYSDEFINED),
  63. tapEventCallback,
  64. self);
  65. assert(_eventPort != NULL);
  66. _eventPortSource = CFMachPortCreateRunLoopSource(kCFAllocatorSystemDefault, _eventPort, 0);
  67. assert(_eventPortSource != NULL);
  68. // Let's do this in a separate thread so that a slow app doesn't lag the event tap
  69. [NSThread detachNewThreadSelector:@selector(eventTapThread) toTarget:self withObject:nil];
  70. }
  71. -(void)stopWatchingMediaKeys;
  72. {
  73. // TODO<nevyn>: Shut down thread, remove event tap port and source
  74. if(_tapThreadRL){
  75. CFRunLoopStop(_tapThreadRL);
  76. _tapThreadRL=nil;
  77. }
  78. if(_eventPort){
  79. CFMachPortInvalidate(_eventPort);
  80. CFRelease(_eventPort);
  81. _eventPort=nil;
  82. }
  83. if(_eventPortSource){
  84. CFRelease(_eventPortSource);
  85. _eventPortSource=nil;
  86. }
  87. }
  88. #pragma mark -
  89. #pragma mark Accessors
  90. +(BOOL)usesGlobalMediaKeyTap
  91. {
  92. #ifdef _DEBUG
  93. // breaking in gdb with a key tap inserted sometimes locks up all mouse and keyboard input forever, forcing reboot
  94. return NO;
  95. #else
  96. // XXX(nevyn): MediaKey event tap doesn't work on 10.4, feel free to figure out why if you have the energy.
  97. return
  98. ![[NSUserDefaults standardUserDefaults] boolForKey:kIgnoreMediaKeysDefaultsKey]
  99. && floor(NSAppKitVersionNumber) >= 949/*NSAppKitVersionNumber10_5*/;
  100. #endif
  101. }
  102. + (NSArray*)defaultMediaKeyUserBundleIdentifiers;
  103. {
  104. return [NSArray arrayWithObjects:
  105. [[NSBundle mainBundle] bundleIdentifier], // your app
  106. @"com.spotify.client",
  107. @"com.apple.iTunes",
  108. @"com.apple.QuickTimePlayerX",
  109. @"com.apple.quicktimeplayer",
  110. @"com.apple.iWork.Keynote",
  111. @"com.apple.iPhoto",
  112. @"org.videolan.vlc",
  113. @"com.apple.Aperture",
  114. @"com.plexsquared.Plex",
  115. @"com.soundcloud.desktop",
  116. @"org.niltsh.MPlayerX",
  117. @"com.ilabs.PandorasHelper",
  118. @"com.mahasoftware.pandabar",
  119. @"com.bitcartel.pandorajam",
  120. @"org.clementine-player.clementine",
  121. @"fm.last.Last.fm",
  122. @"com.beatport.BeatportPro",
  123. @"com.Timenut.SongKey",
  124. @"com.macromedia.fireworks", // the tap messes up their mouse input
  125. nil
  126. ];
  127. }
  128. -(BOOL)shouldInterceptMediaKeyEvents;
  129. {
  130. BOOL shouldIntercept = NO;
  131. @synchronized(self) {
  132. shouldIntercept = _shouldInterceptMediaKeyEvents;
  133. }
  134. return shouldIntercept;
  135. }
  136. -(void)pauseTapOnTapThread:(BOOL)yeahno;
  137. {
  138. CGEventTapEnable(self->_eventPort, yeahno);
  139. }
  140. -(void)setShouldInterceptMediaKeyEvents:(BOOL)newSetting;
  141. {
  142. BOOL oldSetting;
  143. @synchronized(self) {
  144. oldSetting = _shouldInterceptMediaKeyEvents;
  145. _shouldInterceptMediaKeyEvents = newSetting;
  146. }
  147. if(_tapThreadRL && oldSetting != newSetting) {
  148. id grab = [self grab];
  149. [grab pauseTapOnTapThread:newSetting];
  150. NSTimer *timer = [NSTimer timerWithTimeInterval:0 invocation:[grab invocation] repeats:NO];
  151. CFRunLoopAddTimer(_tapThreadRL, (CFRunLoopTimerRef)timer, kCFRunLoopCommonModes);
  152. }
  153. }
  154. #pragma mark
  155. #pragma mark -
  156. #pragma mark Event tap callbacks
  157. // Note: method called on background thread
  158. static CGEventRef tapEventCallback2(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
  159. {
  160. SPMediaKeyTap *self = refcon;
  161. if(type == kCGEventTapDisabledByTimeout) {
  162. NSLog(@"Media key event tap was disabled by timeout");
  163. CGEventTapEnable(self->_eventPort, TRUE);
  164. return event;
  165. } else if(type == kCGEventTapDisabledByUserInput) {
  166. // Was disabled manually by -[pauseTapOnTapThread]
  167. return event;
  168. }
  169. NSEvent *nsEvent = nil;
  170. @try {
  171. nsEvent = [NSEvent eventWithCGEvent:event];
  172. }
  173. @catch (NSException * e) {
  174. NSLog(@"Strange CGEventType: %d: %@", type, e);
  175. assert(0);
  176. return event;
  177. }
  178. if (type != NX_SYSDEFINED || [nsEvent subtype] != SPSystemDefinedEventMediaKeys)
  179. return event;
  180. int keyCode = (([nsEvent data1] & 0xFFFF0000) >> 16);
  181. if (keyCode != NX_KEYTYPE_PLAY && keyCode != NX_KEYTYPE_FAST && keyCode != NX_KEYTYPE_REWIND)
  182. return event;
  183. if (![self shouldInterceptMediaKeyEvents])
  184. return event;
  185. [nsEvent retain]; // matched in handleAndReleaseMediaKeyEvent:
  186. [self performSelectorOnMainThread:@selector(handleAndReleaseMediaKeyEvent:) withObject:nsEvent waitUntilDone:NO];
  187. return NULL;
  188. }
  189. static CGEventRef tapEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
  190. {
  191. NSAutoreleasePool *pool = [NSAutoreleasePool new];
  192. CGEventRef ret = tapEventCallback2(proxy, type, event, refcon);
  193. [pool drain];
  194. return ret;
  195. }
  196. // event will have been retained in the other thread
  197. -(void)handleAndReleaseMediaKeyEvent:(NSEvent *)event {
  198. [event autorelease];
  199. [_delegate mediaKeyTap:self receivedMediaKeyEvent:event];
  200. }
  201. -(void)eventTapThread;
  202. {
  203. _tapThreadRL = CFRunLoopGetCurrent();
  204. CFRunLoopAddSource(_tapThreadRL, _eventPortSource, kCFRunLoopCommonModes);
  205. CFRunLoopRun();
  206. }
  207. #pragma mark Task switching callbacks
  208. NSString *kMediaKeyUsingBundleIdentifiersDefaultsKey = @"SPApplicationsNeedingMediaKeys";
  209. NSString *kIgnoreMediaKeysDefaultsKey = @"SPIgnoreMediaKeys";
  210. -(void)mediaKeyAppListChanged;
  211. {
  212. if([_mediaKeyAppList count] == 0) return;
  213. /*NSLog(@"--");
  214. int i = 0;
  215. for (NSValue *psnv in _mediaKeyAppList) {
  216. ProcessSerialNumber psn; [psnv getValue:&psn];
  217. NSDictionary *processInfo = [(id)ProcessInformationCopyDictionary(
  218. &psn,
  219. kProcessDictionaryIncludeAllInformationMask
  220. ) autorelease];
  221. NSString *bundleIdentifier = [processInfo objectForKey:(id)kCFBundleIdentifierKey];
  222. NSLog(@"%d: %@", i++, bundleIdentifier);
  223. }*/
  224. ProcessSerialNumber mySerial, topSerial;
  225. GetCurrentProcess(&mySerial);
  226. [[_mediaKeyAppList objectAtIndex:0] getValue:&topSerial];
  227. Boolean same;
  228. OSErr err = SameProcess(&mySerial, &topSerial, &same);
  229. [self setShouldInterceptMediaKeyEvents:(err == noErr && same)];
  230. }
  231. -(void)appIsNowFrontmost:(ProcessSerialNumber)psn;
  232. {
  233. NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)];
  234. NSDictionary *processInfo = [(id)ProcessInformationCopyDictionary(
  235. &psn,
  236. kProcessDictionaryIncludeAllInformationMask
  237. ) autorelease];
  238. NSString *bundleIdentifier = [processInfo objectForKey:(id)kCFBundleIdentifierKey];
  239. NSArray *whitelistIdentifiers = [[NSUserDefaults standardUserDefaults] arrayForKey:kMediaKeyUsingBundleIdentifiersDefaultsKey];
  240. if(![whitelistIdentifiers containsObject:bundleIdentifier]) return;
  241. [_mediaKeyAppList removeObject:psnv];
  242. [_mediaKeyAppList insertObject:psnv atIndex:0];
  243. [self mediaKeyAppListChanged];
  244. }
  245. -(void)appTerminated:(ProcessSerialNumber)psn;
  246. {
  247. NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)];
  248. [_mediaKeyAppList removeObject:psnv];
  249. [self mediaKeyAppListChanged];
  250. }
  251. static pascal OSStatus appSwitched (EventHandlerCallRef nextHandler, EventRef evt, void* userData)
  252. {
  253. SPMediaKeyTap *self = (id)userData;
  254. ProcessSerialNumber newSerial;
  255. GetFrontProcess(&newSerial);
  256. [self appIsNowFrontmost:newSerial];
  257. return CallNextEventHandler(nextHandler, evt);
  258. }
  259. static pascal OSStatus appTerminated (EventHandlerCallRef nextHandler, EventRef evt, void* userData)
  260. {
  261. SPMediaKeyTap *self = (id)userData;
  262. ProcessSerialNumber deadPSN;
  263. GetEventParameter(
  264. evt,
  265. kEventParamProcessID,
  266. typeProcessSerialNumber,
  267. NULL,
  268. sizeof(deadPSN),
  269. NULL,
  270. &deadPSN
  271. );
  272. [self appTerminated:deadPSN];
  273. return CallNextEventHandler(nextHandler, evt);
  274. }
  275. @end