]> git.sesse.net Git - vlc/blob - modules/gui/macosx/SPMediaKeyTap.m
macosx: lock access to addon_entry_t
[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             nil
162     ];
163 }
164
165
166 -(BOOL)shouldInterceptMediaKeyEvents;
167 {
168     BOOL shouldIntercept = NO;
169     @synchronized(self) {
170         shouldIntercept = _shouldInterceptMediaKeyEvents;
171     }
172     return shouldIntercept;
173 }
174
175 -(void)pauseTapOnTapThread:(BOOL)yeahno;
176 {
177     CGEventTapEnable(self->_eventPort, yeahno);
178 }
179 -(void)setShouldInterceptMediaKeyEvents:(BOOL)newSetting;
180 {
181     BOOL oldSetting;
182     @synchronized(self) {
183         oldSetting = _shouldInterceptMediaKeyEvents;
184         _shouldInterceptMediaKeyEvents = newSetting;
185     }
186     if(_tapThreadRL && oldSetting != newSetting) {
187         id grab = [self grab];
188         [grab pauseTapOnTapThread:newSetting];
189         NSTimer *timer = [NSTimer timerWithTimeInterval:0 invocation:[grab invocation] repeats:NO];
190         CFRunLoopAddTimer(_tapThreadRL, (CFRunLoopTimerRef)timer, kCFRunLoopCommonModes);
191     }
192 }
193
194 #pragma mark
195 #pragma mark -
196 #pragma mark Event tap callbacks
197
198 // Note: method called on background thread
199
200 static CGEventRef tapEventCallback2(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
201 {
202     SPMediaKeyTap *self = refcon;
203
204     if(type == kCGEventTapDisabledByTimeout) {
205         NSLog(@"Media key event tap was disabled by timeout");
206         CGEventTapEnable(self->_eventPort, TRUE);
207         return event;
208     } else if(type == kCGEventTapDisabledByUserInput) {
209         // Was disabled manually by -[pauseTapOnTapThread]
210         return event;
211     }
212     NSEvent *nsEvent = nil;
213     @try {
214         nsEvent = [NSEvent eventWithCGEvent:event];
215     }
216     @catch (NSException * e) {
217         NSLog(@"Strange CGEventType: %d: %@", type, e);
218         assert(0);
219         return event;
220     }
221
222     if (type != NX_SYSDEFINED || [nsEvent subtype] != SPSystemDefinedEventMediaKeys)
223         return event;
224
225     int keyCode = (([nsEvent data1] & 0xFFFF0000) >> 16);
226     if (keyCode != NX_KEYTYPE_PLAY && keyCode != NX_KEYTYPE_FAST && keyCode != NX_KEYTYPE_REWIND && keyCode != NX_KEYTYPE_PREVIOUS && keyCode != NX_KEYTYPE_NEXT)
227         return event;
228
229     if (![self shouldInterceptMediaKeyEvents])
230         return event;
231
232     [nsEvent retain]; // matched in handleAndReleaseMediaKeyEvent:
233     [self performSelectorOnMainThread:@selector(handleAndReleaseMediaKeyEvent:) withObject:nsEvent waitUntilDone:NO];
234
235     return NULL;
236 }
237
238 static CGEventRef tapEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
239 {
240     NSAutoreleasePool *pool = [NSAutoreleasePool new];
241     CGEventRef ret = tapEventCallback2(proxy, type, event, refcon);
242     [pool drain];
243     return ret;
244 }
245
246
247 // event will have been retained in the other thread
248 -(void)handleAndReleaseMediaKeyEvent:(NSEvent *)event {
249     [event autorelease];
250
251     [_delegate mediaKeyTap:self receivedMediaKeyEvent:event];
252 }
253
254
255 -(void)eventTapThread;
256 {
257     _tapThreadRL = CFRunLoopGetCurrent();
258     CFRunLoopAddSource(_tapThreadRL, _eventPortSource, kCFRunLoopCommonModes);
259     CFRunLoopRun();
260 }
261
262 #pragma mark Task switching callbacks
263
264 NSString *kMediaKeyUsingBundleIdentifiersDefaultsKey = @"SPApplicationsNeedingMediaKeys";
265 NSString *kIgnoreMediaKeysDefaultsKey = @"SPIgnoreMediaKeys";
266
267
268
269 -(void)mediaKeyAppListChanged;
270 {
271     if([_mediaKeyAppList count] == 0) return;
272
273     /*NSLog(@"--");
274     int i = 0;
275     for (NSValue *psnv in _mediaKeyAppList) {
276         ProcessSerialNumber psn; [psnv getValue:&psn];
277         NSDictionary *processInfo = [(id)ProcessInformationCopyDictionary(
278             &psn,
279             kProcessDictionaryIncludeAllInformationMask
280         ) autorelease];
281         NSString *bundleIdentifier = [processInfo objectForKey:(id)kCFBundleIdentifierKey];
282         NSLog(@"%d: %@", i++, bundleIdentifier);
283     }*/
284
285     ProcessSerialNumber mySerial, topSerial;
286     GetCurrentProcess(&mySerial);
287     [[_mediaKeyAppList objectAtIndex:0] getValue:&topSerial];
288
289     Boolean same;
290     OSErr err = SameProcess(&mySerial, &topSerial, &same);
291     [self setShouldInterceptMediaKeyEvents:(err == noErr && same)];
292 }
293 -(void)appIsNowFrontmost:(ProcessSerialNumber)psn;
294 {
295     NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)];
296
297     NSDictionary *processInfo = [(id)ProcessInformationCopyDictionary(
298         &psn,
299         kProcessDictionaryIncludeAllInformationMask
300     ) autorelease];
301     NSString *bundleIdentifier = [processInfo objectForKey:(id)kCFBundleIdentifierKey];
302
303     NSArray *whitelistIdentifiers = [[NSUserDefaults standardUserDefaults] arrayForKey:kMediaKeyUsingBundleIdentifiersDefaultsKey];
304     if(![whitelistIdentifiers containsObject:bundleIdentifier]) return;
305
306     [_mediaKeyAppList removeObject:psnv];
307     [_mediaKeyAppList insertObject:psnv atIndex:0];
308     [self mediaKeyAppListChanged];
309 }
310 -(void)appTerminated:(ProcessSerialNumber)psn;
311 {
312     NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)];
313     [_mediaKeyAppList removeObject:psnv];
314     [self mediaKeyAppListChanged];
315 }
316
317 static pascal OSStatus appSwitched (EventHandlerCallRef nextHandler, EventRef evt, void* userData)
318 {
319     SPMediaKeyTap *self = (id)userData;
320
321     ProcessSerialNumber newSerial;
322     GetFrontProcess(&newSerial);
323
324     [self appIsNowFrontmost:newSerial];
325
326     return CallNextEventHandler(nextHandler, evt);
327 }
328
329 static pascal OSStatus appTerminated (EventHandlerCallRef nextHandler, EventRef evt, void* userData)
330 {
331     SPMediaKeyTap *self = (id)userData;
332
333     ProcessSerialNumber deadPSN;
334
335     GetEventParameter(
336         evt,
337         kEventParamProcessID,
338         typeProcessSerialNumber,
339         NULL,
340         sizeof(deadPSN),
341         NULL,
342         &deadPSN
343     );
344
345     [self appTerminated:deadPSN];
346     return CallNextEventHandler(nextHandler, evt);
347 }
348
349 @end