growl: Add OS X user notifications as fallback to Growl notifications
[vlc.git] / modules / notify / growl.m
1 /*****************************************************************************
2  * growl.m : growl 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  * Growl specific code, ripped from growlnotify:
28  *
29  * Copyright (c) The Growl Project, 2004-2005
30  * All rights reserved.
31  *
32  * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
33  *
34  * 1. Redistributions of source code must retain the above copyright
35  notice, this list of conditions and the following disclaimer.
36  * 2. Redistributions in binary form must reproduce the above copyright
37  notice, this list of conditions and the following disclaimer in the
38  documentation and/or other materials provided with the distribution.
39  * 3. Neither the name of Growl nor the names of its contributors
40  may be used to endorse or promote products derived from this software
41  without specific prior written permission.
42  *
43  * 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 OWNER 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.
44  *
45  *****************************************************************************/
46
47 /*****************************************************************************
48  * Preamble
49  *****************************************************************************/
50
51 #ifdef HAVE_CONFIG_H
52 # include "config.h"
53 #endif
54
55 #import <Foundation/Foundation.h>
56 #import <Cocoa/Cocoa.h>
57 #import <Growl/Growl.h>
58
59 #define VLC_MODULE_LICENSE VLC_LICENSE_GPL_2_PLUS
60 #include <vlc_common.h>
61 #include <vlc_plugin.h>
62 #include <vlc_playlist.h>
63 #include <vlc_input.h>
64 #include <vlc_meta.h>
65 #include <vlc_interface.h>
66 #include <vlc_url.h>
67
68 /*****************************************************************************
69  * intf_sys_t, VLCGrowlDelegate
70  *****************************************************************************/
71 @interface VLCGrowlDelegate : NSObject <GrowlApplicationBridgeDelegate>
72 {
73     NSString *applicationName;
74     NSString *notificationType;
75     NSMutableDictionary *registrationDictionary;
76     id lastNotification;
77     BOOL isInForeground;
78     intf_thread_t *interfaceThread;
79 }
80
81 - (id)initWithInterfaceThread:(intf_thread_t *)thread;
82 - (void)registerToGrowl;
83 - (void)notifyWithTitle:(const char *)title
84                  artist:(const char *)artist
85                   album:(const char *)album
86               andArtUrl:(const char *)url;
87 @end
88
89 struct intf_sys_t
90 {
91     VLCGrowlDelegate *o_growl_delegate;
92     int             i_id;
93     int             i_item_changes;
94 };
95
96 /*****************************************************************************
97  * Local prototypes
98  *****************************************************************************/
99 static int  Open    ( vlc_object_t * );
100 static void Close   ( vlc_object_t * );
101
102 static int ItemChange( vlc_object_t *, const char *,
103                       vlc_value_t, vlc_value_t, void * );
104
105 /*****************************************************************************
106  * Module descriptor
107  ****************************************************************************/
108
109 vlc_module_begin ()
110 set_category( CAT_INTERFACE )
111 set_subcategory( SUBCAT_INTERFACE_CONTROL )
112 set_shortname( "Growl" )
113 set_description( N_("Growl Notification Plugin") )
114 set_capability( "interface", 0 )
115 set_callbacks( Open, Close )
116 vlc_module_end ()
117
118 /*****************************************************************************
119  * Open: initialize and create stuff
120  *****************************************************************************/
121 static int Open( vlc_object_t *p_this )
122 {
123     intf_thread_t *p_intf = (intf_thread_t *)p_this;
124     playlist_t *p_playlist;
125     intf_sys_t    *p_sys;
126
127     p_sys = p_intf->p_sys = calloc( 1, sizeof(intf_sys_t) );
128     if( !p_sys )
129         return VLC_ENOMEM;
130
131     p_sys->o_growl_delegate = [[VLCGrowlDelegate alloc] initWithInterfaceThread:p_intf];
132     if( !p_sys->o_growl_delegate )
133       return VLC_ENOMEM;
134
135     p_playlist = pl_Get( p_intf );
136     var_AddCallback( p_playlist, "item-change", ItemChange, p_intf );
137     var_AddCallback( p_playlist, "input-current", ItemChange, p_intf );
138
139     [p_sys->o_growl_delegate registerToGrowl];
140     return VLC_SUCCESS;
141 }
142
143 /*****************************************************************************
144  * Close: destroy interface stuff
145  *****************************************************************************/
146 static void Close( vlc_object_t *p_this )
147 {
148     intf_thread_t *p_intf = (intf_thread_t *)p_this;
149     playlist_t *p_playlist = pl_Get( p_intf );
150     intf_sys_t *p_sys = p_intf->p_sys;
151
152     var_DelCallback( p_playlist, "item-change", ItemChange, p_intf );
153     var_DelCallback( p_playlist, "input-current", ItemChange, p_intf );
154
155     [GrowlApplicationBridge setGrowlDelegate:nil];
156     [p_sys->o_growl_delegate release];
157     free( p_sys );
158 }
159
160 /*****************************************************************************
161  * ItemChange: Playlist item change callback
162  *****************************************************************************/
163 static int ItemChange( vlc_object_t *p_this, const char *psz_var,
164                       vlc_value_t oldval, vlc_value_t newval, void *param )
165 {
166     VLC_UNUSED(oldval);
167
168     intf_thread_t *p_intf = (intf_thread_t *)param;
169     char *psz_tmp           = NULL;
170     char *psz_title         = NULL;
171     char *psz_artist        = NULL;
172     char *psz_album         = NULL;
173     input_item_t *p_item = newval.p_address;
174
175     bool b_is_item_current = !strcmp( "input-current", psz_var );
176
177     /* Don't update each time an item has been preparsed */
178     if( b_is_item_current )
179     { /* stores the current input item id */
180         input_thread_t *p_input = newval.p_address;
181         if( !p_input )
182             return VLC_SUCCESS;
183
184         p_item = input_GetItem( p_input );
185         if( p_intf->p_sys->i_id != p_item->i_id )
186         {
187             p_intf->p_sys->i_id = p_item->i_id;
188             p_intf->p_sys->i_item_changes = 0;
189         }
190
191         return VLC_SUCCESS;
192     }
193     /* ignore items which weren't pre-parsed yet */
194     else if( !input_item_IsPreparsed(p_item) )
195         return VLC_SUCCESS;
196     else
197     {   /* "item-change" */
198         
199         if( p_item->i_id != p_intf->p_sys->i_id )
200             return VLC_SUCCESS;
201
202         /* Some variable bitrate inputs call "item-change" callbacks each time
203          * their length is updated, that is several times per second.
204          * We'll limit the number of changes to 1 per input. */
205         if( p_intf->p_sys->i_item_changes > 0 )
206             return VLC_SUCCESS;
207
208         p_intf->p_sys->i_item_changes++;
209     }
210
211     /* Playing something ... */
212     if( input_item_GetNowPlayingFb( p_item ) )
213         psz_title = input_item_GetNowPlayingFb( p_item );
214     else
215         psz_title = input_item_GetTitleFbName( p_item );
216     if( EMPTY_STR( psz_title ) )
217     {
218         free( psz_title );
219         return VLC_SUCCESS;
220     }
221
222     psz_artist = input_item_GetArtist( p_item );
223     if( EMPTY_STR( psz_artist ) ) FREENULL( psz_artist );
224     psz_album = input_item_GetAlbum( p_item ) ;
225     if( EMPTY_STR( psz_album ) ) FREENULL( psz_album );
226
227     int i_ret;
228     if( psz_artist && psz_album )
229         i_ret = asprintf( &psz_tmp, "%s\n%s [%s]",
230                          psz_title, psz_artist, psz_album );
231     else if( psz_artist )
232         i_ret = asprintf( &psz_tmp, "%s\n%s", psz_title, psz_artist );
233     else
234         i_ret = asprintf(&psz_tmp, "%s", psz_title );
235
236     if( i_ret == -1 )
237     {
238         free( psz_title );
239         free( psz_artist );
240         free( psz_album );
241         return VLC_ENOMEM;
242     }
243
244     char *psz_arturl = input_item_GetArtURL( p_item );
245     if( psz_arturl )
246     {
247         char *psz = make_path( psz_arturl );
248         free( psz_arturl );
249         psz_arturl = psz;
250     }
251
252     [p_intf->p_sys->o_growl_delegate notifyWithTitle:psz_title
253                                               artist:psz_artist
254                                                album:psz_album
255                                            andArtUrl:psz_arturl];
256
257     free( psz_title );
258     free( psz_artist );
259     free( psz_album );
260     free( psz_arturl );
261     free( psz_tmp );
262
263     return VLC_SUCCESS;
264 }
265
266 /*****************************************************************************
267  * VLCGrowlDelegate
268  *****************************************************************************/
269 @implementation VLCGrowlDelegate
270
271 - (id)initWithInterfaceThread:(intf_thread_t *)thread {
272     if( !( self = [super init] ) )
273         return nil;
274
275     applicationName = nil;
276     notificationType = nil;
277     registrationDictionary = nil;
278     interfaceThread = thread;
279
280     // Assume we start in foreground
281     isInForeground = YES;
282
283     // Subscribe to notifications to determine if VLC is in foreground or not
284     @autoreleasepool {
285         [[NSNotificationCenter defaultCenter] addObserver:self
286                                                  selector:@selector(applicationActiveChange:)
287                                                      name:NSApplicationDidBecomeActiveNotification
288                                                    object:nil];
289
290         [[NSNotificationCenter defaultCenter] addObserver:self
291                                                  selector:@selector(applicationActiveChange:)
292                                                      name:NSApplicationDidResignActiveNotification
293                                                    object:nil];
294     }
295     return self;
296 }
297
298 - (void)dealloc
299 {
300 #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1080
301     // Clear the remaining lastNotification in Notification Center, if any
302     @autoreleasepool {
303         if (lastNotification) {
304             [NSUserNotificationCenter.defaultUserNotificationCenter
305              removeDeliveredNotification:(NSUserNotification *)lastNotification];
306             [lastNotification release];
307         }
308         [[NSNotificationCenter defaultCenter] removeObserver:self];
309     }
310 #endif
311
312     // Release everything
313     [applicationName release];
314     [notificationType release];
315     [registrationDictionary release];
316     [super dealloc];
317 }
318
319 - (void)registerToGrowl
320 {
321     @autoreleasepool {
322         applicationName = [[NSString alloc] initWithUTF8String:_( "VLC media player" )];
323         notificationType = [[NSString alloc] initWithUTF8String:_( "New input playing" )];
324
325         NSArray *defaultAndAllNotifications = [NSArray arrayWithObject: notificationType];
326         registrationDictionary = [[NSMutableDictionary alloc] init];
327         [registrationDictionary setObject:defaultAndAllNotifications
328                                    forKey:GROWL_NOTIFICATIONS_ALL];
329         [registrationDictionary setObject:defaultAndAllNotifications
330                                    forKey: GROWL_NOTIFICATIONS_DEFAULT];
331
332         [GrowlApplicationBridge setGrowlDelegate:self];
333
334 #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1080
335         [[NSUserNotificationCenter defaultUserNotificationCenter]
336          setDelegate:(id<NSUserNotificationCenterDelegate>)self];
337 #endif
338     }
339 }
340
341 - (void)notifyWithTitle:(const char *)title
342                  artist:(const char *)artist
343                   album:(const char *)album
344               andArtUrl:(const char *)url
345 {
346     @autoreleasepool {
347         // Init Cover
348         NSData *coverImageData = nil;
349         NSImage *coverImage = nil;
350
351         if (url) {
352             coverImageData = [NSData dataWithContentsOfFile:[NSString stringWithUTF8String:url]];
353             coverImage = [[NSImage alloc] initWithData:coverImageData];
354         }
355
356         // Init Track info
357         NSString *titleStr = nil;
358         NSString *artistStr = nil;
359         NSString *albumStr = nil;
360
361         if (title) {
362             titleStr = [NSString stringWithUTF8String:title];
363         } else {
364             // Without title, notification makes no sense, so return here
365             // title should never be empty, but better check than crash.
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         } else {
395 #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1080
396             // Make the OS X notification and string
397             NSUserNotification *notification = [NSUserNotification new];
398             NSString *desc = nil;
399
400             if (artistStr && albumStr) {
401                 desc = [NSString stringWithFormat:@"%@ – %@", artistStr, albumStr];
402             } else if (artistStr) {
403                 desc = artistStr;
404             }
405
406             notification.title              = titleStr;
407             notification.subtitle           = desc;
408             notification.hasActionButton    = YES;
409             notification.actionButtonTitle  = [NSString stringWithUTF8String:_("Skip")];
410
411             // Private APIs to set cover image, see rdar://23148801
412             // and show action button, see rdar://23148733
413             [notification setValue:coverImage forKey:@"_identityImage"];
414             [notification setValue:@(YES) forKey:@"_showsButtons"];
415             [NSUserNotificationCenter.defaultUserNotificationCenter deliverNotification:notification];
416             [notification release];
417 #endif
418         }
419
420         // Release stuff
421         [coverImage release];
422     }
423 }
424
425 /*****************************************************************************
426  * Delegate methods
427  *****************************************************************************/
428 - (NSDictionary *)registrationDictionaryForGrowl
429 {
430     return registrationDictionary;
431 }
432
433 - (NSString *)applicationNameForGrowl
434 {
435     return applicationName;
436 }
437
438 - (void)applicationActiveChange:(NSNotification *)n {
439     if (n.name == NSApplicationDidBecomeActiveNotification)
440         isInForeground = YES;
441     else if (n.name == NSApplicationDidResignActiveNotification)
442         isInForeground = NO;
443 }
444
445 #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1080
446 - (void)userNotificationCenter:(NSUserNotificationCenter *)center
447        didActivateNotification:(NSUserNotification *)notification
448 {
449     if (notification.activationType == NSUserNotificationActivationTypeActionButtonClicked) {
450         playlist_Next(pl_Get(interfaceThread));
451     }
452 }
453
454 - (void)userNotificationCenter:(NSUserNotificationCenter *)center
455         didDeliverNotification:(NSUserNotification *)notification
456 {
457     // Only keep the most recent notification in the Notification Center
458     if (lastNotification) {
459         [center removeDeliveredNotification: (NSUserNotification *)lastNotification];
460         [lastNotification release];
461     }
462     [notification retain];
463     lastNotification = notification;
464 }
465
466 - (BOOL)userNotificationCenter:(NSUserNotificationCenter *)center
467      shouldPresentNotification:(NSUserNotification *)notification
468 {
469     // Show notifications regardless if App in foreground or background
470     return YES;
471 }
472 #endif
473 @end