SPMediaKeyTap.m 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  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. @"fm.last.Scrobbler",
  123. @"com.beatport.BeatportPro",
  124. @"com.Timenut.SongKey",
  125. @"com.macromedia.fireworks", // the tap messes up their mouse input
  126. @"at.justp.Theremin",
  127. @"ru.ya.themblsha.YandexMusic",
  128. @"com.jriver.MediaCenter18",
  129. @"com.jriver.MediaCenter19",
  130. @"com.jriver.MediaCenter20",
  131. @"co.rackit.mate",
  132. @"com.ttitt.b-music",
  133. @"com.beardedspice.BeardedSpice",
  134. @"com.plug.Plug",
  135. @"com.plug.Plug2",
  136. @"com.netease.163music",
  137. nil
  138. ];
  139. }
  140. -(BOOL)shouldInterceptMediaKeyEvents;
  141. {
  142. BOOL shouldIntercept = NO;
  143. @synchronized(self) {
  144. shouldIntercept = _shouldInterceptMediaKeyEvents;
  145. }
  146. return shouldIntercept;
  147. }
  148. -(void)pauseTapOnTapThread:(BOOL)yeahno;
  149. {
  150. CGEventTapEnable(self->_eventPort, yeahno);
  151. }
  152. -(void)setShouldInterceptMediaKeyEvents:(BOOL)newSetting;
  153. {
  154. BOOL oldSetting;
  155. @synchronized(self) {
  156. oldSetting = _shouldInterceptMediaKeyEvents;
  157. _shouldInterceptMediaKeyEvents = newSetting;
  158. }
  159. if(_tapThreadRL && oldSetting != newSetting) {
  160. id grab = [self grab];
  161. [grab pauseTapOnTapThread:newSetting];
  162. NSTimer *timer = [NSTimer timerWithTimeInterval:0 invocation:[grab invocation] repeats:NO];
  163. CFRunLoopAddTimer(_tapThreadRL, (CFRunLoopTimerRef)timer, kCFRunLoopCommonModes);
  164. }
  165. }
  166. #pragma mark
  167. #pragma mark -
  168. #pragma mark Event tap callbacks
  169. // Note: method called on background thread
  170. static CGEventRef tapEventCallback2(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
  171. {
  172. SPMediaKeyTap *self = refcon;
  173. if(type == kCGEventTapDisabledByTimeout) {
  174. NSLog(@"Media key event tap was disabled by timeout");
  175. CGEventTapEnable(self->_eventPort, TRUE);
  176. return event;
  177. } else if(type == kCGEventTapDisabledByUserInput) {
  178. // Was disabled manually by -[pauseTapOnTapThread]
  179. return event;
  180. }
  181. NSEvent *nsEvent = nil;
  182. @try {
  183. nsEvent = [NSEvent eventWithCGEvent:event];
  184. }
  185. @catch (NSException * e) {
  186. NSLog(@"Strange CGEventType: %d: %@", type, e);
  187. assert(0);
  188. return event;
  189. }
  190. if (type != NX_SYSDEFINED || [nsEvent subtype] != SPSystemDefinedEventMediaKeys)
  191. return event;
  192. int keyCode = (([nsEvent data1] & 0xFFFF0000) >> 16);
  193. if (keyCode != NX_KEYTYPE_PLAY && keyCode != NX_KEYTYPE_FAST && keyCode != NX_KEYTYPE_REWIND && keyCode != NX_KEYTYPE_PREVIOUS && keyCode != NX_KEYTYPE_NEXT)
  194. return event;
  195. if (![self shouldInterceptMediaKeyEvents])
  196. return event;
  197. [nsEvent retain]; // matched in handleAndReleaseMediaKeyEvent:
  198. [self performSelectorOnMainThread:@selector(handleAndReleaseMediaKeyEvent:) withObject:nsEvent waitUntilDone:NO];
  199. return NULL;
  200. }
  201. static CGEventRef tapEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
  202. {
  203. NSAutoreleasePool *pool = [NSAutoreleasePool new];
  204. CGEventRef ret = tapEventCallback2(proxy, type, event, refcon);
  205. [pool drain];
  206. return ret;
  207. }
  208. // event will have been retained in the other thread
  209. -(void)handleAndReleaseMediaKeyEvent:(NSEvent *)event {
  210. [event autorelease];
  211. [_delegate mediaKeyTap:self receivedMediaKeyEvent:event];
  212. }
  213. -(void)eventTapThread;
  214. {
  215. _tapThreadRL = CFRunLoopGetCurrent();
  216. CFRunLoopAddSource(_tapThreadRL, _eventPortSource, kCFRunLoopCommonModes);
  217. CFRunLoopRun();
  218. }
  219. #pragma mark Task switching callbacks
  220. NSString *kMediaKeyUsingBundleIdentifiersDefaultsKey = @"SPApplicationsNeedingMediaKeys";
  221. NSString *kIgnoreMediaKeysDefaultsKey = @"SPIgnoreMediaKeys";
  222. -(void)mediaKeyAppListChanged;
  223. {
  224. if([_mediaKeyAppList count] == 0) return;
  225. /*NSLog(@"--");
  226. int i = 0;
  227. for (NSValue *psnv in _mediaKeyAppList) {
  228. ProcessSerialNumber psn; [psnv getValue:&psn];
  229. NSDictionary *processInfo = [(id)ProcessInformationCopyDictionary(
  230. &psn,
  231. kProcessDictionaryIncludeAllInformationMask
  232. ) autorelease];
  233. NSString *bundleIdentifier = [processInfo objectForKey:(id)kCFBundleIdentifierKey];
  234. NSLog(@"%d: %@", i++, bundleIdentifier);
  235. }*/
  236. ProcessSerialNumber mySerial, topSerial;
  237. GetCurrentProcess(&mySerial);
  238. [[_mediaKeyAppList objectAtIndex:0] getValue:&topSerial];
  239. Boolean same;
  240. OSErr err = SameProcess(&mySerial, &topSerial, &same);
  241. [self setShouldInterceptMediaKeyEvents:(err == noErr && same)];
  242. }
  243. -(void)appIsNowFrontmost:(ProcessSerialNumber)psn;
  244. {
  245. NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)];
  246. NSDictionary *processInfo = [(id)ProcessInformationCopyDictionary(
  247. &psn,
  248. kProcessDictionaryIncludeAllInformationMask
  249. ) autorelease];
  250. NSString *bundleIdentifier = [processInfo objectForKey:(id)kCFBundleIdentifierKey];
  251. NSArray *whitelistIdentifiers = [[NSUserDefaults standardUserDefaults] arrayForKey:kMediaKeyUsingBundleIdentifiersDefaultsKey];
  252. if(![whitelistIdentifiers containsObject:bundleIdentifier]) return;
  253. [_mediaKeyAppList removeObject:psnv];
  254. [_mediaKeyAppList insertObject:psnv atIndex:0];
  255. [self mediaKeyAppListChanged];
  256. }
  257. -(void)appTerminated:(ProcessSerialNumber)psn;
  258. {
  259. NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)];
  260. [_mediaKeyAppList removeObject:psnv];
  261. [self mediaKeyAppListChanged];
  262. }
  263. static pascal OSStatus appSwitched (EventHandlerCallRef nextHandler, EventRef evt, void* userData)
  264. {
  265. SPMediaKeyTap *self = (id)userData;
  266. ProcessSerialNumber newSerial;
  267. GetFrontProcess(&newSerial);
  268. [self appIsNowFrontmost:newSerial];
  269. return CallNextEventHandler(nextHandler, evt);
  270. }
  271. static pascal OSStatus appTerminated (EventHandlerCallRef nextHandler, EventRef evt, void* userData)
  272. {
  273. SPMediaKeyTap *self = (id)userData;
  274. ProcessSerialNumber deadPSN;
  275. GetEventParameter(
  276. evt,
  277. kEventParamProcessID,
  278. typeProcessSerialNumber,
  279. NULL,
  280. sizeof(deadPSN),
  281. NULL,
  282. &deadPSN
  283. );
  284. [self appTerminated:deadPSN];
  285. return CallNextEventHandler(nextHandler, evt);
  286. }
  287. @end