5cb4758116b701f7b3cb81fa3ef2510c3d0d426b
[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 #pragma clang diagnostic push
276 #pragma clang diagnostic ignored "-Wpartial-availability"
277 #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1080
278     // Clear the remaining lastNotification in Notification Center, if any
279     @autoreleasepool {
280         if (lastNotification && hasNativeNotifications) {
281             [NSUserNotificationCenter.defaultUserNotificationCenter
282              removeDeliveredNotification:(NSUserNotification *)lastNotification];
283             [lastNotification release];
284         }
285         [[NSNotificationCenter defaultCenter] removeObserver:self];
286     }
287 #endif
288 #pragma clang diagnostic pop
289
290     // Release everything
291     [applicationName release];
292     [notificationType release];
293     [registrationDictionary release];
294     [super dealloc];
295 }
296
297 - (void)registerToGrowl
298 {
299     @autoreleasepool {
300         applicationName = [[NSString alloc] initWithUTF8String:_( "VLC media player" )];
301         notificationType = [[NSString alloc] initWithUTF8String:_( "New input playing" )];
302
303         NSArray *defaultAndAllNotifications = [NSArray arrayWithObject: notificationType];
304         registrationDictionary = [[NSMutableDictionary alloc] init];
305         [registrationDictionary setObject:defaultAndAllNotifications
306                                    forKey:GROWL_NOTIFICATIONS_ALL];
307         [registrationDictionary setObject:defaultAndAllNotifications
308                                    forKey: GROWL_NOTIFICATIONS_DEFAULT];
309
310         [GrowlApplicationBridge setGrowlDelegate:self];
311
312 #pragma clang diagnostic push
313 #pragma clang diagnostic ignored "-Wpartial-availability"
314 #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1080
315         if (hasNativeNotifications) {
316             [[NSUserNotificationCenter defaultUserNotificationCenter]
317              setDelegate:(id<NSUserNotificationCenterDelegate>)self];
318         }
319 #endif
320 #pragma clang diagnostic pop
321     }
322 }
323
324 - (void)notifyWithTitle:(const char *)title
325                  artist:(const char *)artist
326                   album:(const char *)album
327               andArtUrl:(const char *)url
328 {
329     @autoreleasepool {
330         // Do not notify if in foreground
331         if (isInForeground)
332             return;
333
334         // Init Cover
335         NSData *coverImageData = nil;
336         NSImage *coverImage = nil;
337
338         if (url) {
339             coverImageData = [NSData dataWithContentsOfFile:[NSString stringWithUTF8String:url]];
340             coverImage = [[NSImage alloc] initWithData:coverImageData];
341         }
342
343         // Init Track info
344         NSString *titleStr = nil;
345         NSString *artistStr = nil;
346         NSString *albumStr = nil;
347
348         if (title) {
349             titleStr = [NSString stringWithUTF8String:title];
350         } else {
351             // Without title, notification makes no sense, so return here
352             // title should never be empty, but better check than crash.
353             [coverImage release];
354             return;
355         }
356         if (artist)
357             artistStr = [NSString stringWithUTF8String:artist];
358         if (album)
359             albumStr = [NSString stringWithUTF8String:album];
360
361         // Notification stuff
362         if ([GrowlApplicationBridge isGrowlRunning]) {
363             // Make the Growl notification string
364             NSString *desc = nil;
365
366             if (artistStr && albumStr) {
367                 desc = [NSString stringWithFormat:@"%@\n%@ [%@]", titleStr, artistStr, albumStr];
368             } else if (artistStr) {
369                 desc = [NSString stringWithFormat:@"%@\n%@", titleStr, artistStr];
370             } else {
371                 desc = titleStr;
372             }
373
374             // Send notification
375             [GrowlApplicationBridge notifyWithTitle:[NSString stringWithUTF8String:_("Now playing")]
376                                         description:desc
377                                    notificationName:notificationType
378                                            iconData:coverImageData
379                                            priority:0
380                                            isSticky:NO
381                                        clickContext:nil
382                                          identifier:@"VLCNowPlayingNotification"];
383         } else if (hasNativeNotifications) {
384 #pragma clang diagnostic push
385 #pragma clang diagnostic ignored "-Wpartial-availability"
386 #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1080
387             // Make the OS X notification and string
388             NSUserNotification *notification = [NSUserNotification new];
389             NSString *desc = nil;
390
391             if (artistStr && albumStr) {
392                 desc = [NSString stringWithFormat:@"%@ – %@", artistStr, albumStr];
393             } else if (artistStr) {
394                 desc = artistStr;
395             }
396
397             notification.title              = titleStr;
398             notification.subtitle           = desc;
399             notification.hasActionButton    = YES;
400             notification.actionButtonTitle  = [NSString stringWithUTF8String:_("Skip")];
401
402             // Private APIs to set cover image, see rdar://23148801
403             // and show action button, see rdar://23148733
404             [notification setValue:coverImage forKey:@"_identityImage"];
405             [notification setValue:@(YES) forKey:@"_showsButtons"];
406             [NSUserNotificationCenter.defaultUserNotificationCenter deliverNotification:notification];
407             [notification release];
408 #endif
409 #pragma clang diagnostic pop
410         }
411
412         // Release stuff
413         [coverImage release];
414     }
415 }
416
417 /*****************************************************************************
418  * Delegate methods
419  *****************************************************************************/
420 - (NSDictionary *)registrationDictionaryForGrowl
421 {
422     return registrationDictionary;
423 }
424
425 - (NSString *)applicationNameForGrowl
426 {
427     return applicationName;
428 }
429
430 - (void)applicationActiveChange:(NSNotification *)n {
431     if (n.name == NSApplicationDidBecomeActiveNotification)
432         isInForeground = YES;
433     else if (n.name == NSApplicationDidResignActiveNotification)
434         isInForeground = NO;
435 }
436
437 #pragma clang diagnostic push
438 #pragma clang diagnostic ignored "-Wpartial-availability"
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 #pragma clang diagnostic pop
462 @end