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