]> git.sesse.net Git - vlc/blob - modules/gui/macosx/SPMediaKeyTap.m
d5c681e1f187bd84c70f3cebe97fcaa473ae2051
[vlc] / modules / gui / macosx / SPMediaKeyTap.m
1 /*
2  Copyright (c) 2011, Joachim Bengtsson
3  All rights reserved.
4
5  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
6
7  * Neither the name of the organization nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
8
9  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
10 */
11
12 // Copyright (c) 2010 Spotify AB
13 #import "SPMediaKeyTap.h"
14 #import "SPInvocationGrabbing.h"
15
16 @interface SPMediaKeyTap ()
17 -(BOOL)shouldInterceptMediaKeyEvents;
18 -(void)setShouldInterceptMediaKeyEvents:(BOOL)newSetting;
19 -(void)startWatchingAppSwitching;
20 -(void)stopWatchingAppSwitching;
21 -(void)eventTapThread;
22 @end
23 static SPMediaKeyTap *singleton = nil;
24
25 static pascal OSStatus appSwitched (EventHandlerCallRef nextHandler, EventRef evt, void* userData);
26 static pascal OSStatus appTerminated (EventHandlerCallRef nextHandler, EventRef evt, void* userData);
27 static CGEventRef tapEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon);
28
29
30 // Inspired by http://gist.github.com/546311
31
32 @implementation SPMediaKeyTap
33
34 #pragma mark -
35 #pragma mark Setup and teardown
36 -(id)initWithDelegate:(id)delegate;
37 {
38     _delegate = delegate;
39     [self startWatchingAppSwitching];
40     singleton = self;
41     _mediaKeyAppList = [NSMutableArray new];
42     _tapThreadRL=nil;
43     _eventPort=nil;
44     _eventPortSource=nil;
45     return self;
46 }
47 -(void)dealloc;
48 {
49     [self stopWatchingMediaKeys];
50     [self stopWatchingAppSwitching];
51     [_mediaKeyAppList release];
52     [super dealloc];
53 }
54
55 -(void)startWatchingAppSwitching;
56 {
57     // Listen to "app switched" event, so that we don't intercept media keys if we
58     // weren't the last "media key listening" app to be active
59     EventTypeSpec eventType = { kEventClassApplication, kEventAppFrontSwitched };
60     OSStatus err = InstallApplicationEventHandler(NewEventHandlerUPP(appSwitched), 1, &eventType, self, &_app_switching_ref);
61     assert(err == noErr);
62
63     eventType.eventKind = kEventAppTerminated;
64     err = InstallApplicationEventHandler(NewEventHandlerUPP(appTerminated), 1, &eventType, self, &_app_terminating_ref);
65     assert(err == noErr);
66 }
67 -(void)stopWatchingAppSwitching;
68 {
69     if(!_app_switching_ref) return;
70     RemoveEventHandler(_app_switching_ref);
71     _app_switching_ref = NULL;
72 }
73
74 -(void)startWatchingMediaKeys;{
75     // Prevent having multiple mediaKeys threads
76     [self stopWatchingMediaKeys];
77
78     [self setShouldInterceptMediaKeyEvents:YES];
79
80     // Add an event tap to intercept the system defined media key events
81     _eventPort = CGEventTapCreate(kCGSessionEventTap,
82                                   kCGHeadInsertEventTap,
83                                   kCGEventTapOptionDefault,
84                                   CGEventMaskBit(NX_SYSDEFINED),
85                                   tapEventCallback,
86                                   self);
87     assert(_eventPort != NULL);
88
89     _eventPortSource = CFMachPortCreateRunLoopSource(kCFAllocatorSystemDefault, _eventPort, 0);
90     assert(_eventPortSource != NULL);
91
92     // Let's do this in a separate thread so that a slow app doesn't lag the event tap
93     [NSThread detachNewThreadSelector:@selector(eventTapThread) toTarget:self withObject:nil];
94 }
95 -(void)stopWatchingMediaKeys;
96 {
97     // TODO<nevyn>: Shut down thread, remove event tap port and source
98
99     if(_tapThreadRL){
100         CFRunLoopStop(_tapThreadRL);
101         _tapThreadRL=nil;
102     }
103
104     if(_eventPort){
105         CFMachPortInvalidate(_eventPort);
106         CFRelease(_eventPort);
107         _eventPort=nil;
108     }
109
110     if(_eventPortSource){
111         CFRelease(_eventPortSource);
112         _eventPortSource=nil;
113     }
114 }
115
116 #pragma mark -
117 #pragma mark Accessors
118
119 +(BOOL)usesGlobalMediaKeyTap
120 {
121 #ifdef _DEBUG
122     // breaking in gdb with a key tap inserted sometimes locks up all mouse and keyboard input forever, forcing reboot
123     return NO;
124 #else
125     // XXX(nevyn): MediaKey event tap doesn't work on 10.4, feel free to figure out why if you have the energy.
126     return
127         ![[NSUserDefaults standardUserDefaults] boolForKey:kIgnoreMediaKeysDefaultsKey]
128         && floor(NSAppKitVersionNumber) >= 949/*NSAppKitVersionNumber10_5*/;
129 #endif
130 }
131
132 + (NSArray*)defaultMediaKeyUserBundleIdentifiers;
133 {
134     return [NSArray arrayWithObjects:
135             [[NSBundle mainBundle] bundleIdentifier], // your app
136              @"com.spotify.client",
137              @"com.apple.iTunes",
138              @"com.apple.QuickTimePlayerX",
139              @"com.apple.quicktimeplayer",
140              @"com.apple.iWork.Keynote",
141              @"com.apple.iPhoto",
142              @"org.videolan.vlc",
143              @"com.apple.Aperture",
144              @"com.plexsquared.Plex",
145              @"com.soundcloud.desktop",
146              @"org.niltsh.MPlayerX",
147              @"com.ilabs.PandorasHelper",
148              @"com.mahasoftware.pandabar",
149              @"com.bitcartel.pandorajam",
150              @"org.clementine-player.clementine",
151              @"fm.last.Last.fm",
152              @"fm.last.Scrobbler",
153              @"com.beatport.BeatportPro",
154              @"com.Timenut.SongKey",
155              @"com.macromedia.fireworks", // the tap messes up their mouse input
156              @"at.justp.Theremin",
157              @"ru.ya.themblsha.YandexMusic",
158              @"com.jriver.MediaCenter18",
159              @"com.jriver.MediaCenter19",
160              @"com.jriver.MediaCenter20",
161              @"co.rackit.mate",
162              @"com.ttitt.b-music",
163              @"com.beardedspice.BeardedSpice",
164              @"com.plug.Plug",
165              @"com.netease.163music",
166             nil
167     ];
168 }
169
170
171 -(BOOL)shouldInterceptMediaKeyEvents;
172 {
173     BOOL shouldIntercept = NO;
174     @synchronized(self) {
175         shouldIntercept = _shouldInterceptMediaKeyEvents;
176     }
177     return shouldIntercept;
178 }
179
180 -(void)pauseTapOnTapThread:(BOOL)yeahno;
181 {
182     CGEventTapEnable(self->_eventPort, yeahno);
183 }
184 -(void)setShouldInterceptMediaKeyEvents:(BOOL)newSetting;
185 {
186     BOOL oldSetting;
187     @synchronized(self) {
188         oldSetting = _shouldInterceptMediaKeyEvents;
189         _shouldInterceptMediaKeyEvents = newSetting;
190     }
191     if(_tapThreadRL && oldSetting != newSetting) {
192         id grab = [self grab];
193         [grab pauseTapOnTapThread:newSetting];
194         NSTimer *timer = [NSTimer timerWithTimeInterval:0 invocation:[grab invocation] repeats:NO];
195         CFRunLoopAddTimer(_tapThreadRL, (CFRunLoopTimerRef)timer, kCFRunLoopCommonModes);
196     }
197 }
198
199 #pragma mark
200 #pragma mark -
201 #pragma mark Event tap callbacks
202
203 // Note: method called on background thread
204
205 static CGEventRef tapEventCallback2(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
206 {
207     SPMediaKeyTap *self = refcon;
208
209     if(type == kCGEventTapDisabledByTimeout) {
210         NSLog(@"Media key event tap was disabled by timeout");
211         CGEventTapEnable(self->_eventPort, TRUE);
212         return event;
213     } else if(type == kCGEventTapDisabledByUserInput) {
214         // Was disabled manually by -[pauseTapOnTapThread]
215         return event;
216     }
217     NSEvent *nsEvent = nil;
218     @try {
219         nsEvent = [NSEvent eventWithCGEvent:event];
220     }
221     @catch (NSException * e) {
222         NSLog(@"Strange CGEventType: %d: %@", type, e);
223         assert(0);
224         return event;
225     }
226
227     if (type != NX_SYSDEFINED || [nsEvent subtype] != SPSystemDefinedEventMediaKeys)
228         return event;
229
230     int keyCode = (([nsEvent data1] & 0xFFFF0000) >> 16);
231     if (keyCode != NX_KEYTYPE_PLAY && keyCode != NX_KEYTYPE_FAST && keyCode != NX_KEYTYPE_REWIND && keyCode != NX_KEYTYPE_PREVIOUS && keyCode != NX_KEYTYPE_NEXT)
232         return event;
233
234     if (![self shouldInterceptMediaKeyEvents])
235         return event;
236
237     [nsEvent retain]; // matched in handleAndReleaseMediaKeyEvent:
238     [self performSelectorOnMainThread:@selector(handleAndReleaseMediaKeyEvent:) withObject:nsEvent waitUntilDone:NO];
239
240     return NULL;
241 }
242
243 static CGEventRef tapEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
244 {
245     NSAutoreleasePool *pool = [NSAutoreleasePool new];
246     CGEventRef ret = tapEventCallback2(proxy, type, event, refcon);
247     [pool drain];
248     return ret;
249 }
250
251
252 // event will have been retained in the other thread
253 -(void)handleAndReleaseMediaKeyEvent:(NSEvent *)event {
254     [event autorelease];
255
256     [_delegate mediaKeyTap:self receivedMediaKeyEvent:event];
257 }
258
259
260 -(void)eventTapThread;
261 {
262     _tapThreadRL = CFRunLoopGetCurrent();
263     CFRunLoopAddSource(_tapThreadRL, _eventPortSource, kCFRunLoopCommonModes);
264     CFRunLoopRun();
265 }
266
267 #pragma mark Task switching callbacks
268
269 NSString *kMediaKeyUsingBundleIdentifiersDefaultsKey = @"SPApplicationsNeedingMediaKeys";
270 NSString *kIgnoreMediaKeysDefaultsKey = @"SPIgnoreMediaKeys";
271
272
273
274 -(void)mediaKeyAppListChanged;
275 {
276     if([_mediaKeyAppList count] == 0) return;
277
278     /*NSLog(@"--");
279     int i = 0;
280     for (NSValue *psnv in _mediaKeyAppList) {
281         ProcessSerialNumber psn; [psnv getValue:&psn];
282         NSDictionary *processInfo = [(id)ProcessInformationCopyDictionary(
283             &psn,
284             kProcessDictionaryIncludeAllInformationMask
285         ) autorelease];
286         NSString *bundleIdentifier = [processInfo objectForKey:(id)kCFBundleIdentifierKey];
287         NSLog(@"%d: %@", i++, bundleIdentifier);
288     }*/
289
290     ProcessSerialNumber mySerial, topSerial;
291     GetCurrentProcess(&mySerial);
292     [[_mediaKeyAppList objectAtIndex:0] getValue:&topSerial];
293
294     Boolean same;
295     OSErr err = SameProcess(&mySerial, &topSerial, &same);
296     [self setShouldInterceptMediaKeyEvents:(err == noErr && same)];
297 }
298 -(void)appIsNowFrontmost:(ProcessSerialNumber)psn;
299 {
300     NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)];
301
302     NSDictionary *processInfo = [(id)ProcessInformationCopyDictionary(
303         &psn,
304         kProcessDictionaryIncludeAllInformationMask
305     ) autorelease];
306     NSString *bundleIdentifier = [processInfo objectForKey:(id)kCFBundleIdentifierKey];
307
308     NSArray *whitelistIdentifiers = [[NSUserDefaults standardUserDefaults] arrayForKey:kMediaKeyUsingBundleIdentifiersDefaultsKey];
309     if(![whitelistIdentifiers containsObject:bundleIdentifier]) return;
310
311     [_mediaKeyAppList removeObject:psnv];
312     [_mediaKeyAppList insertObject:psnv atIndex:0];
313     [self mediaKeyAppListChanged];
314 }
315 -(void)appTerminated:(ProcessSerialNumber)psn;
316 {
317     NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)];
318     [_mediaKeyAppList removeObject:psnv];
319     [self mediaKeyAppListChanged];
320 }
321
322 static pascal OSStatus appSwitched (EventHandlerCallRef nextHandler, EventRef evt, void* userData)
323 {
324     SPMediaKeyTap *self = (id)userData;
325
326     ProcessSerialNumber newSerial;
327     GetFrontProcess(&newSerial);
328
329     [self appIsNowFrontmost:newSerial];
330
331     return CallNextEventHandler(nextHandler, evt);
332 }
333
334 static pascal OSStatus appTerminated (EventHandlerCallRef nextHandler, EventRef evt, void* userData)
335 {
336     SPMediaKeyTap *self = (id)userData;
337
338     ProcessSerialNumber deadPSN;
339
340     GetEventParameter(
341         evt,
342         kEventParamProcessID,
343         typeProcessSerialNumber,
344         NULL,
345         sizeof(deadPSN),
346         NULL,
347         &deadPSN
348     );
349
350     [self appTerminated:deadPSN];
351     return CallNextEventHandler(nextHandler, evt);
352 }
353
354 @end