20f7d0701a05fbacf43d4e7c27d5d19fa285ffe5
[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     intf_thread_t *interfaceThread;
90 }
91
92 - (id)initWithInterfaceThread:(intf_thread_t *)thread;
93 - (void)registerToGrowl;
94 - (void)notifyWithTitle:(const char *)title
95                  artist:(const char *)artist
96                   album:(const char *)album
97               andArtUrl:(const char *)url;
98 @end
99
100 struct intf_sys_t
101 {
102     VLCGrowlDelegate *o_growl_delegate;
103     int             i_id;
104     int             i_item_changes;
105 };
106
107 /*****************************************************************************
108  * Local prototypes
109  *****************************************************************************/
110 static int  Open    ( vlc_object_t * );
111 static void Close   ( vlc_object_t * );
112
113 static int ItemChange( vlc_object_t *, const char *,
114                       vlc_value_t, vlc_value_t, void * );
115
116 /*****************************************************************************
117  * Module descriptor
118  ****************************************************************************/
119
120 vlc_module_begin ()
121 set_category( CAT_INTERFACE )
122 set_subcategory( SUBCAT_INTERFACE_CONTROL )
123 set_shortname( "OSX-Notifications" )
124 add_shortcut( "growl" )
125 set_description( N_("OS X Notification Plugin") )
126 set_capability( "interface", 0 )
127 set_callbacks( Open, Close )
128 vlc_module_end ()
129
130 /*****************************************************************************
131  * Open: initialize and create stuff
132  *****************************************************************************/
133 static int Open( vlc_object_t *p_this )
134 {
135     intf_thread_t *p_intf = (intf_thread_t *)p_this;
136     playlist_t *p_playlist;
137     intf_sys_t    *p_sys;
138
139     p_sys = p_intf->p_sys = calloc( 1, sizeof(intf_sys_t) );
140     if( !p_sys )
141         return VLC_ENOMEM;
142
143     p_sys->o_growl_delegate = [[VLCGrowlDelegate alloc] initWithInterfaceThread:p_intf];
144     if( !p_sys->o_growl_delegate )
145       return VLC_ENOMEM;
146
147     p_playlist = pl_Get( p_intf );
148     var_AddCallback( p_playlist, "item-change", ItemChange, p_intf );
149     var_AddCallback( p_playlist, "input-current", ItemChange, p_intf );
150
151     [p_sys->o_growl_delegate registerToGrowl];
152     return VLC_SUCCESS;
153 }
154
155 /*****************************************************************************
156  * Close: destroy interface stuff
157  *****************************************************************************/
158 static void Close( vlc_object_t *p_this )
159 {
160     intf_thread_t *p_intf = (intf_thread_t *)p_this;
161     playlist_t *p_playlist = pl_Get( p_intf );
162     intf_sys_t *p_sys = p_intf->p_sys;
163
164     var_DelCallback( p_playlist, "item-change", ItemChange, p_intf );
165     var_DelCallback( p_playlist, "input-current", ItemChange, p_intf );
166
167     [GrowlApplicationBridge setGrowlDelegate:nil];
168     [p_sys->o_growl_delegate release];
169     free( p_sys );
170 }
171
172 /*****************************************************************************
173  * ItemChange: Playlist item change callback
174  *****************************************************************************/
175 static int ItemChange( vlc_object_t *p_this, const char *psz_var,
176                       vlc_value_t oldval, vlc_value_t newval, void *param )
177 {
178     VLC_UNUSED(oldval);
179
180     intf_thread_t *p_intf = (intf_thread_t *)param;
181     char *psz_tmp           = NULL;
182     char *psz_title         = NULL;
183     char *psz_artist        = NULL;
184     char *psz_album         = NULL;
185     input_item_t *p_item = newval.p_address;
186
187     bool b_is_item_current = !strcmp( "input-current", psz_var );
188
189     /* Don't update each time an item has been preparsed */
190     if( b_is_item_current )
191     { /* stores the current input item id */
192         input_thread_t *p_input = newval.p_address;
193         if( !p_input )
194             return VLC_SUCCESS;
195
196         p_item = input_GetItem( p_input );
197         if( p_intf->p_sys->i_id != p_item->i_id )
198         {
199             p_intf->p_sys->i_id = p_item->i_id;
200             p_intf->p_sys->i_item_changes = 0;
201         }
202
203         return VLC_SUCCESS;
204     }
205     /* ignore items which weren't pre-parsed yet */
206     else if( !input_item_IsPreparsed(p_item) )
207         return VLC_SUCCESS;
208     else
209     {   /* "item-change" */
210         if( p_item->i_id != p_intf->p_sys->i_id )
211             return VLC_SUCCESS;
212
213         /* Some variable bitrate inputs call "item-change" callbacks each time
214          * their length is updated, that is several times per second.
215          * We'll limit the number of changes to 1 per input. */
216         if( p_intf->p_sys->i_item_changes > 0 )
217             return VLC_SUCCESS;
218
219         p_intf->p_sys->i_item_changes++;
220     }
221
222     /* Playing something ... */
223     if( input_item_GetNowPlayingFb( p_item ) )
224         psz_title = input_item_GetNowPlayingFb( p_item );
225     else
226         psz_title = input_item_GetTitleFbName( p_item );
227     if( EMPTY_STR( psz_title ) )
228     {
229         free( psz_title );
230         return VLC_SUCCESS;
231     }
232
233     psz_artist = input_item_GetArtist( p_item );
234     if( EMPTY_STR( psz_artist ) ) FREENULL( psz_artist );
235     psz_album = input_item_GetAlbum( p_item ) ;
236     if( EMPTY_STR( psz_album ) ) FREENULL( psz_album );
237
238     int i_ret;
239     if( psz_artist && psz_album )
240         i_ret = asprintf( &psz_tmp, "%s\n%s [%s]",
241                          psz_title, psz_artist, psz_album );
242     else if( psz_artist )
243         i_ret = asprintf( &psz_tmp, "%s\n%s", psz_title, psz_artist );
244     else
245         i_ret = asprintf(&psz_tmp, "%s", psz_title );
246
247     if( i_ret == -1 )
248     {
249         free( psz_title );
250         free( psz_artist );
251         free( psz_album );
252         return VLC_ENOMEM;
253     }
254
255     char *psz_arturl = input_item_GetArtURL( p_item );
256     if( psz_arturl )
257     {
258         char *psz = make_path( psz_arturl );
259         free( psz_arturl );
260         psz_arturl = psz;
261     }
262
263     [p_intf->p_sys->o_growl_delegate notifyWithTitle:psz_title
264                                               artist:psz_artist
265                                                album:psz_album
266                                            andArtUrl:psz_arturl];
267
268     free( psz_title );
269     free( psz_artist );
270     free( psz_album );
271     free( psz_arturl );
272     free( psz_tmp );
273
274     return VLC_SUCCESS;
275 }
276
277 /*****************************************************************************
278  * VLCGrowlDelegate
279  *****************************************************************************/
280 @implementation VLCGrowlDelegate
281
282 - (id)initWithInterfaceThread:(intf_thread_t *)thread {
283     if( !( self = [super init] ) )
284         return nil;
285
286     applicationName = nil;
287     notificationType = nil;
288     registrationDictionary = nil;
289     interfaceThread = thread;
290
291     return self;
292 }
293
294 - (void)dealloc
295 {
296 #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1080
297     // Clear the remaining lastNotification in Notification Center, if any
298     @autoreleasepool {
299         if (lastNotification) {
300             [NSUserNotificationCenter.defaultUserNotificationCenter
301              removeDeliveredNotification:(NSUserNotification *)lastNotification];
302             [lastNotification release];
303         }
304     }
305 #endif
306
307     // Release everything
308     [applicationName release];
309     [notificationType release];
310     [registrationDictionary release];
311     [super dealloc];
312 }
313
314 - (void)registerToGrowl
315 {
316     @autoreleasepool {
317         applicationName = [[NSString alloc] initWithUTF8String:_( "VLC media player" )];
318         notificationType = [[NSString alloc] initWithUTF8String:_( "New input playing" )];
319
320         NSArray *defaultAndAllNotifications = [NSArray arrayWithObject: notificationType];
321         registrationDictionary = [[NSMutableDictionary alloc] init];
322         [registrationDictionary setObject:defaultAndAllNotifications
323                                    forKey:GROWL_NOTIFICATIONS_ALL];
324         [registrationDictionary setObject:defaultAndAllNotifications
325                                    forKey: GROWL_NOTIFICATIONS_DEFAULT];
326
327         [GrowlApplicationBridge setGrowlDelegate:self];
328
329 #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1080
330         [[NSUserNotificationCenter defaultUserNotificationCenter]
331          setDelegate:(id<NSUserNotificationCenterDelegate>)self];
332 #endif
333     }
334 }
335
336 - (void)notifyWithTitle:(const char *)title
337                  artist:(const char *)artist
338                   album:(const char *)album
339               andArtUrl:(const char *)url
340 {
341     @autoreleasepool {
342         // Do not notify if in foreground
343         if ([NSApplication sharedApplication].active)
344             return;
345
346         // Init Cover
347         NSData *coverImageData = nil;
348         NSImage *coverImage = nil;
349
350         if (url) {
351             coverImageData = [NSData dataWithContentsOfFile:[NSString stringWithUTF8String:url]];
352             coverImage = [[NSImage alloc] initWithData:coverImageData];
353         }
354
355         // Init Track info
356         NSString *titleStr = nil;
357         NSString *artistStr = nil;
358         NSString *albumStr = nil;
359
360         if (title) {
361             titleStr = [NSString stringWithUTF8String:title];
362         } else {
363             // Without title, notification makes no sense, so return here
364             // title should never be empty, but better check than crash.
365             [coverImage release];
366             return;
367         }
368         if (artist)
369             artistStr = [NSString stringWithUTF8String:artist];
370         if (album)
371             albumStr = [NSString stringWithUTF8String:album];
372
373         // Notification stuff
374         if ([GrowlApplicationBridge isGrowlRunning]) {
375             // Make the Growl notification string
376             NSString *desc = nil;
377
378             if (artistStr && albumStr) {
379                 desc = [NSString stringWithFormat:@"%@\n%@ [%@]", titleStr, artistStr, albumStr];
380             } else if (artistStr) {
381                 desc = [NSString stringWithFormat:@"%@\n%@", titleStr, artistStr];
382             } else {
383                 desc = titleStr;
384             }
385
386             // Send notification
387             [GrowlApplicationBridge notifyWithTitle:[NSString stringWithUTF8String:_("Now playing")]
388                                         description:desc
389                                    notificationName:notificationType
390                                            iconData:coverImageData
391                                            priority:0
392                                            isSticky:NO
393                                        clickContext:nil
394                                          identifier:@"VLCNowPlayingNotification"];
395         } else {
396 #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1080
397             // Make the OS X notification and string
398             NSUserNotification *notification = [NSUserNotification new];
399             NSString *desc = nil;
400
401             if (artistStr && albumStr) {
402                 desc = [NSString stringWithFormat:@"%@ – %@", artistStr, albumStr];
403             } else if (artistStr) {
404                 desc = artistStr;
405             }
406
407             notification.title              = titleStr;
408             notification.subtitle           = desc;
409             notification.hasActionButton    = YES;
410             notification.actionButtonTitle  = [NSString stringWithUTF8String:_("Skip")];
411
412             // Private APIs to set cover image, see rdar://23148801
413             // and show action button, see rdar://23148733
414             [notification setValue:coverImage forKey:@"_identityImage"];
415             [notification setValue:@(YES) forKey:@"_showsButtons"];
416             [NSUserNotificationCenter.defaultUserNotificationCenter deliverNotification:notification];
417             [notification release];
418 #endif
419         }
420
421         // Release stuff
422         [coverImage release];
423     }
424 }
425
426 /*****************************************************************************
427  * Delegate methods
428  *****************************************************************************/
429 - (NSDictionary *)registrationDictionaryForGrowl
430 {
431     return registrationDictionary;
432 }
433
434 - (NSString *)applicationNameForGrowl
435 {
436     return applicationName;
437 }
438
439 #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1080
440 - (void)userNotificationCenter:(NSUserNotificationCenter *)center
441        didActivateNotification:(NSUserNotification *)notification
442 {
443     // Skip to next song
444     if (notification.activationType == NSUserNotificationActivationTypeActionButtonClicked) {
445         playlist_Next(pl_Get(interfaceThread));
446     }
447 }
448
449 - (void)userNotificationCenter:(NSUserNotificationCenter *)center
450         didDeliverNotification:(NSUserNotification *)notification
451 {
452     // Only keep the most recent notification in the Notification Center
453     if (lastNotification) {
454         [center removeDeliveredNotification: (NSUserNotification *)lastNotification];
455         [lastNotification release];
456     }
457     [notification retain];
458     lastNotification = notification;
459 }
460 #endif
461 @end