osx_notifications: Use active state notifications for compatibility with OS X <10.10
[vlc.git] / modules / notify / osx_notifications.m
1 /*****************************************************************************
2  * osx_notifications.m : OS X notification plugin
3  *****************************************************************************
4  * VLC specific code:
5  *
6  * Copyright © 2008,2011,2012,2015 the VideoLAN team
7  * $Id$
8  *
9  * Authors: Rafaël Carré <funman@videolanorg>
10  *          Felix Paul Kühne <fkuehne@videolan.org
11  *          Marvin Scholz <epirat07@gmail.com>
12  *
13  * This program is free software; you can redistribute it and/or modify
14  * it under the terms of the GNU General Public License as published by
15  * the Free Software Foundation; either version 2 of the License, or
16  * (at your option) any later version.
17  *
18  * This program is distributed in the hope that it will be useful,
19  * but WITHOUT ANY WARRANTY; without even the implied warranty of
20  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21  * GNU General Public License for more details.
22  *
23  * You should have received a copy of the GNU General Public License
24  * along with this program; if not, write to the Free Software
25  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
26  *
27  * ---
28  *
29  * Growl specific code, ripped from growlnotify:
30  *
31  * Copyright (c) The Growl Project, 2004-2005
32  * All rights reserved.
33  *
34  * Redistribution and use in source and binary forms, with or without modification,
35  * are permitted provided that the following conditions are met:
36  *
37  * 1. Redistributions of source code must retain the above copyright
38  *    notice, this list of conditions and the following disclaimer.
39  * 2. Redistributions in binary form must reproduce the above copyright
40  *    notice, this list of conditions and the following disclaimer in the
41  *    documentation and/or other materials provided with the distribution.
42  * 3. Neither the name of Growl nor the names of its contributors
43  *    may be used to endorse or promote products derived from this software
44  *    without specific prior written permission.
45  *
46  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
47  * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
48  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
49  * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
50  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
51  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
52  * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
53  * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
54  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
55  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
56  *
57  *****************************************************************************/
58
59 /*****************************************************************************
60  * Preamble
61  *****************************************************************************/
62
63 #ifdef HAVE_CONFIG_H
64 # include "config.h"
65 #endif
66
67 #import <Foundation/Foundation.h>
68 #import <Cocoa/Cocoa.h>
69 #import <Growl/Growl.h>
70
71 #define VLC_MODULE_LICENSE VLC_LICENSE_GPL_2_PLUS
72 #include <vlc_common.h>
73 #include <vlc_plugin.h>
74 #include <vlc_playlist.h>
75 #include <vlc_input.h>
76 #include <vlc_meta.h>
77 #include <vlc_interface.h>
78 #include <vlc_url.h>
79
80 /*****************************************************************************
81  * intf_sys_t, VLCGrowlDelegate
82  *****************************************************************************/
83 @interface VLCGrowlDelegate : NSObject <GrowlApplicationBridgeDelegate>
84 {
85     NSString *applicationName;
86     NSString *notificationType;
87     NSMutableDictionary *registrationDictionary;
88     id lastNotification;
89     bool isInForeground;
90     intf_thread_t *interfaceThread;
91 }
92
93 - (id)initWithInterfaceThread:(intf_thread_t *)thread;
94 - (void)registerToGrowl;
95 - (void)notifyWithTitle:(const char *)title
96                  artist:(const char *)artist
97                   album:(const char *)album
98               andArtUrl:(const char *)url;
99 @end
100
101 struct intf_sys_t
102 {
103     VLCGrowlDelegate *o_growl_delegate;
104     int             i_id;
105     int             i_item_changes;
106 };
107
108 /*****************************************************************************
109  * Local prototypes
110  *****************************************************************************/
111 static int  Open    ( vlc_object_t * );
112 static void Close   ( vlc_object_t * );
113
114 static int ItemChange( vlc_object_t *, const char *,
115                       vlc_value_t, vlc_value_t, void * );
116
117 /*****************************************************************************
118  * Module descriptor
119  ****************************************************************************/
120
121 vlc_module_begin ()
122 set_category( CAT_INTERFACE )
123 set_subcategory( SUBCAT_INTERFACE_CONTROL )
124 set_shortname( "OSX-Notifications" )
125 add_shortcut( "growl" )
126 set_description( N_("OS X Notification Plugin") )
127 set_capability( "interface", 0 )
128 set_callbacks( Open, Close )
129 vlc_module_end ()
130
131 /*****************************************************************************
132  * Open: initialize and create stuff
133  *****************************************************************************/
134 static int Open( vlc_object_t *p_this )
135 {
136     intf_thread_t *p_intf = (intf_thread_t *)p_this;
137     playlist_t *p_playlist;
138     intf_sys_t    *p_sys;
139
140     p_sys = p_intf->p_sys = calloc( 1, sizeof(intf_sys_t) );
141     if( !p_sys )
142         return VLC_ENOMEM;
143
144     p_sys->o_growl_delegate = [[VLCGrowlDelegate alloc] initWithInterfaceThread:p_intf];
145     if( !p_sys->o_growl_delegate )
146       return VLC_ENOMEM;
147
148     p_playlist = pl_Get( p_intf );
149     var_AddCallback( p_playlist, "item-change", ItemChange, p_intf );
150     var_AddCallback( p_playlist, "input-current", ItemChange, p_intf );
151
152     [p_sys->o_growl_delegate registerToGrowl];
153     return VLC_SUCCESS;
154 }
155
156 /*****************************************************************************
157  * Close: destroy interface stuff
158  *****************************************************************************/
159 static void Close( vlc_object_t *p_this )
160 {
161     intf_thread_t *p_intf = (intf_thread_t *)p_this;
162     playlist_t *p_playlist = pl_Get( p_intf );
163     intf_sys_t *p_sys = p_intf->p_sys;
164
165     var_DelCallback( p_playlist, "item-change", ItemChange, p_intf );
166     var_DelCallback( p_playlist, "input-current", ItemChange, p_intf );
167
168     [GrowlApplicationBridge setGrowlDelegate:nil];
169     [p_sys->o_growl_delegate release];
170     free( p_sys );
171 }
172
173 /*****************************************************************************
174  * ItemChange: Playlist item change callback
175  *****************************************************************************/
176 static int ItemChange( vlc_object_t *p_this, const char *psz_var,
177                       vlc_value_t oldval, vlc_value_t newval, void *param )
178 {
179     VLC_UNUSED(oldval);
180
181     intf_thread_t *p_intf = (intf_thread_t *)param;
182     char *psz_tmp           = NULL;
183     char *psz_title         = NULL;
184     char *psz_artist        = NULL;
185     char *psz_album         = NULL;
186     input_item_t *p_item = newval.p_address;
187
188     bool b_is_item_current = !strcmp( "input-current", psz_var );
189
190     /* Don't update each time an item has been preparsed */
191     if( b_is_item_current )
192     { /* stores the current input item id */
193         input_thread_t *p_input = newval.p_address;
194         if( !p_input )
195             return VLC_SUCCESS;
196
197         p_item = input_GetItem( p_input );
198         if( p_intf->p_sys->i_id != p_item->i_id )
199         {
200             p_intf->p_sys->i_id = p_item->i_id;
201             p_intf->p_sys->i_item_changes = 0;
202         }
203
204         return VLC_SUCCESS;
205     }
206     /* ignore items which weren't pre-parsed yet */
207     else if( !input_item_IsPreparsed(p_item) )
208         return VLC_SUCCESS;
209     else
210     {   /* "item-change" */
211         if( p_item->i_id != p_intf->p_sys->i_id )
212             return VLC_SUCCESS;
213
214         /* Some variable bitrate inputs call "item-change" callbacks each time
215          * their length is updated, that is several times per second.
216          * We'll limit the number of changes to 1 per input. */
217         if( p_intf->p_sys->i_item_changes > 0 )
218             return VLC_SUCCESS;
219
220         p_intf->p_sys->i_item_changes++;
221     }
222
223     /* Playing something ... */
224     if( input_item_GetNowPlayingFb( p_item ) )
225         psz_title = input_item_GetNowPlayingFb( p_item );
226     else
227         psz_title = input_item_GetTitleFbName( p_item );
228     if( EMPTY_STR( psz_title ) )
229     {
230         free( psz_title );
231         return VLC_SUCCESS;
232     }
233
234     psz_artist = input_item_GetArtist( p_item );
235     if( EMPTY_STR( psz_artist ) ) FREENULL( psz_artist );
236     psz_album = input_item_GetAlbum( p_item ) ;
237     if( EMPTY_STR( psz_album ) ) FREENULL( psz_album );
238
239     int i_ret;
240     if( psz_artist && psz_album )
241         i_ret = asprintf( &psz_tmp, "%s\n%s [%s]",
242                          psz_title, psz_artist, psz_album );
243     else if( psz_artist )
244         i_ret = asprintf( &psz_tmp, "%s\n%s", psz_title, psz_artist );
245     else
246         i_ret = asprintf(&psz_tmp, "%s", psz_title );
247
248     if( i_ret == -1 )
249     {
250         free( psz_title );
251         free( psz_artist );
252         free( psz_album );
253         return VLC_ENOMEM;
254     }
255
256     char *psz_arturl = input_item_GetArtURL( p_item );
257     if( psz_arturl )
258     {
259         char *psz = make_path( psz_arturl );
260         free( psz_arturl );
261         psz_arturl = psz;
262     }
263
264     [p_intf->p_sys->o_growl_delegate notifyWithTitle:psz_title
265                                               artist:psz_artist
266                                                album:psz_album
267                                            andArtUrl:psz_arturl];
268
269     free( psz_title );
270     free( psz_artist );
271     free( psz_album );
272     free( psz_arturl );
273     free( psz_tmp );
274
275     return VLC_SUCCESS;
276 }
277
278 /*****************************************************************************
279  * VLCGrowlDelegate
280  *****************************************************************************/
281 @implementation VLCGrowlDelegate
282
283 - (id)initWithInterfaceThread:(intf_thread_t *)thread {
284     if( !( self = [super init] ) )
285         return nil;
286
287     @autoreleasepool {
288         // Subscribe to notifications to determine if VLC is in foreground or not
289         [[NSNotificationCenter defaultCenter] addObserver:self
290                                                  selector:@selector(applicationActiveChange:)
291                                                      name:NSApplicationDidBecomeActiveNotification
292                                                    object:nil];
293
294         [[NSNotificationCenter defaultCenter] addObserver:self
295                                                  selector:@selector(applicationActiveChange:)
296                                                      name:NSApplicationDidResignActiveNotification
297                                                    object:nil];
298     }
299     // Start in background
300     isInForeground = NO;
301
302     applicationName = nil;
303     notificationType = nil;
304     registrationDictionary = nil;
305     interfaceThread = thread;
306
307     return self;
308 }
309
310 - (void)dealloc
311 {
312 #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1080
313     // Clear the remaining lastNotification in Notification Center, if any
314     @autoreleasepool {
315         if (lastNotification) {
316             [NSUserNotificationCenter.defaultUserNotificationCenter
317              removeDeliveredNotification:(NSUserNotification *)lastNotification];
318             [lastNotification release];
319         }
320         [[NSNotificationCenter defaultCenter] removeObserver:self];
321     }
322 #endif
323
324     // Release everything
325     [applicationName release];
326     [notificationType release];
327     [registrationDictionary release];
328     [super dealloc];
329 }
330
331 - (void)registerToGrowl
332 {
333     @autoreleasepool {
334         applicationName = [[NSString alloc] initWithUTF8String:_( "VLC media player" )];
335         notificationType = [[NSString alloc] initWithUTF8String:_( "New input playing" )];
336
337         NSArray *defaultAndAllNotifications = [NSArray arrayWithObject: notificationType];
338         registrationDictionary = [[NSMutableDictionary alloc] init];
339         [registrationDictionary setObject:defaultAndAllNotifications
340                                    forKey:GROWL_NOTIFICATIONS_ALL];
341         [registrationDictionary setObject:defaultAndAllNotifications
342                                    forKey: GROWL_NOTIFICATIONS_DEFAULT];
343
344         [GrowlApplicationBridge setGrowlDelegate:self];
345
346 #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1080
347         [[NSUserNotificationCenter defaultUserNotificationCenter]
348          setDelegate:(id<NSUserNotificationCenterDelegate>)self];
349 #endif
350     }
351 }
352
353 - (void)notifyWithTitle:(const char *)title
354                  artist:(const char *)artist
355                   album:(const char *)album
356               andArtUrl:(const char *)url
357 {
358     @autoreleasepool {
359         // Do not notify if in foreground
360         if (isInForeground)
361             return;
362
363         // Init Cover
364         NSData *coverImageData = nil;
365         NSImage *coverImage = nil;
366
367         if (url) {
368             coverImageData = [NSData dataWithContentsOfFile:[NSString stringWithUTF8String:url]];
369             coverImage = [[NSImage alloc] initWithData:coverImageData];
370         }
371
372         // Init Track info
373         NSString *titleStr = nil;
374         NSString *artistStr = nil;
375         NSString *albumStr = nil;
376
377         if (title) {
378             titleStr = [NSString stringWithUTF8String:title];
379         } else {
380             // Without title, notification makes no sense, so return here
381             // title should never be empty, but better check than crash.
382             [coverImage release];
383             return;
384         }
385         if (artist)
386             artistStr = [NSString stringWithUTF8String:artist];
387         if (album)
388             albumStr = [NSString stringWithUTF8String:album];
389
390         // Notification stuff
391         if ([GrowlApplicationBridge isGrowlRunning]) {
392             // Make the Growl notification string
393             NSString *desc = nil;
394
395             if (artistStr && albumStr) {
396                 desc = [NSString stringWithFormat:@"%@\n%@ [%@]", titleStr, artistStr, albumStr];
397             } else if (artistStr) {
398                 desc = [NSString stringWithFormat:@"%@\n%@", titleStr, artistStr];
399             } else {
400                 desc = titleStr;
401             }
402
403             // Send notification
404             [GrowlApplicationBridge notifyWithTitle:[NSString stringWithUTF8String:_("Now playing")]
405                                         description:desc
406                                    notificationName:notificationType
407                                            iconData:coverImageData
408                                            priority:0
409                                            isSticky:NO
410                                        clickContext:nil
411                                          identifier:@"VLCNowPlayingNotification"];
412         } else {
413 #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1080
414             // Make the OS X notification and string
415             NSUserNotification *notification = [NSUserNotification new];
416             NSString *desc = nil;
417
418             if (artistStr && albumStr) {
419                 desc = [NSString stringWithFormat:@"%@ – %@", artistStr, albumStr];
420             } else if (artistStr) {
421                 desc = artistStr;
422             }
423
424             notification.title              = titleStr;
425             notification.subtitle           = desc;
426             notification.hasActionButton    = YES;
427             notification.actionButtonTitle  = [NSString stringWithUTF8String:_("Skip")];
428
429             // Private APIs to set cover image, see rdar://23148801
430             // and show action button, see rdar://23148733
431             [notification setValue:coverImage forKey:@"_identityImage"];
432             [notification setValue:@(YES) forKey:@"_showsButtons"];
433             [NSUserNotificationCenter.defaultUserNotificationCenter deliverNotification:notification];
434             [notification release];
435 #endif
436         }
437
438         // Release stuff
439         [coverImage release];
440     }
441 }
442
443 /*****************************************************************************
444  * Delegate methods
445  *****************************************************************************/
446 - (NSDictionary *)registrationDictionaryForGrowl
447 {
448     return registrationDictionary;
449 }
450
451 - (NSString *)applicationNameForGrowl
452 {
453     return applicationName;
454 }
455
456 - (void)applicationActiveChange:(NSNotification *)n {
457     if (n.name == NSApplicationDidBecomeActiveNotification)
458         isInForeground = YES;
459     else if (n.name == NSApplicationDidResignActiveNotification)
460         isInForeground = NO;
461 }
462
463 #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1080
464 - (void)userNotificationCenter:(NSUserNotificationCenter *)center
465        didActivateNotification:(NSUserNotification *)notification
466 {
467     // Skip to next song
468     if (notification.activationType == NSUserNotificationActivationTypeActionButtonClicked) {
469         playlist_Next(pl_Get(interfaceThread));
470     }
471 }
472
473 - (void)userNotificationCenter:(NSUserNotificationCenter *)center
474         didDeliverNotification:(NSUserNotification *)notification
475 {
476     // Only keep the most recent notification in the Notification Center
477     if (lastNotification) {
478         [center removeDeliveredNotification: (NSUserNotification *)lastNotification];
479         [lastNotification release];
480     }
481     [notification retain];
482     lastNotification = notification;
483 }
484 #endif
485 @end