PageRenderTime 226ms CodeModel.GetById 40ms app.highlight 145ms RepoModel.GetById 36ms app.codeStats 0ms

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