notify/osx_notifications: Fix build
[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 #pragma clang diagnostic ignored "-Wunguarded-availability"
64
65 #ifdef HAVE_CONFIG_H
66 # include "config.h"
67 #endif
68
69 #import <Foundation/Foundation.h>
70 #import <Cocoa/Cocoa.h>
71 #import <Growl/Growl.h>
72
73 #define VLC_MODULE_LICENSE VLC_LICENSE_GPL_2_PLUS
74 #include <vlc_common.h>
75 #include <vlc_plugin.h>
76 #include <vlc_playlist.h>
77 #include <vlc_input.h>
78 #include <vlc_meta.h>
79 #include <vlc_interface.h>
80 #include <vlc_url.h>
81
82 /*****************************************************************************
83  * intf_sys_t, VLCGrowlDelegate
84  *****************************************************************************/
85 @interface VLCGrowlDelegate : NSObject <GrowlApplicationBridgeDelegate>
86 {
87     NSString *applicationName;
88     NSString *notificationType;
89     NSMutableDictionary *registrationDictionary;
90     id lastNotification;
91     bool isInForeground;
92     bool hasNativeNotifications;
93     intf_thread_t *interfaceThread;
94 }
95
96 - (id)initWithInterfaceThread:(intf_thread_t *)thread;
97 - (void)registerToGrowl;
98 - (void)notifyWithTitle:(const char *)title
99                  artist:(const char *)artist
100                   album:(const char *)album
101               andArtUrl:(const char *)url;
102 @end
103
104 struct intf_sys_t
105 {
106     VLCGrowlDelegate *o_growl_delegate;
107 };
108
109 /*****************************************************************************
110  * Local prototypes
111  *****************************************************************************/
112 static int  Open    ( vlc_object_t * );
113 static void Close   ( vlc_object_t * );
114
115 static int InputCurrent( vlc_object_t *, const char *,
116                       vlc_value_t, vlc_value_t, void * );
117
118 /*****************************************************************************
119  * Module descriptor
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 = pl_Get( p_intf );
138     intf_sys_t *p_sys = p_intf->p_sys = calloc( 1, sizeof(intf_sys_t) );
139
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     {
146         free( p_sys );
147         return VLC_ENOMEM;
148     }
149
150     var_AddCallback( p_playlist, "input-current", InputCurrent, 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, "input-current", InputCurrent, p_intf );
166
167     [GrowlApplicationBridge setGrowlDelegate:nil];
168     [p_sys->o_growl_delegate release];
169     free( p_sys );
170 }
171
172 /*****************************************************************************
173  * InputCurrent: Current playlist item changed callback
174  *****************************************************************************/
175 static int InputCurrent( 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     intf_sys_t *p_sys = p_intf->p_sys;
182     input_thread_t *p_input = newval.p_address;
183     char *psz_title = NULL;
184     char *psz_artist = NULL;
185     char *psz_album = NULL;
186     char *psz_arturl = NULL;
187
188     if( !p_input )
189         return VLC_SUCCESS;
190
191     input_item_t *p_item = input_GetItem( p_input );
192     if( !p_item )
193         return VLC_SUCCESS;
194
195     /* Get title */
196     psz_title = input_item_GetNowPlayingFb( p_item );
197     if( !psz_title )
198         psz_title = input_item_GetTitleFbName( p_item );
199
200     if( EMPTY_STR( psz_title ) )
201     {
202         free( psz_title );
203         return VLC_SUCCESS;
204     }
205
206     /* Get Artist name */
207     psz_artist = input_item_GetArtist( p_item );
208     if( EMPTY_STR( psz_artist ) )
209         FREENULL( psz_artist );
210
211     /* Get Album name */
212     psz_album = input_item_GetAlbum( p_item ) ;
213     if( EMPTY_STR( psz_album ) )
214         FREENULL( psz_album );
215
216     /* Get Art path */
217     psz_arturl = input_item_GetArtURL( p_item );
218     if( psz_arturl )
219     {
220         char *psz = vlc_uri2path( psz_arturl );
221         free( psz_arturl );
222         psz_arturl = psz;
223     }
224
225     [p_sys->o_growl_delegate notifyWithTitle:psz_title
226                                       artist:psz_artist
227                                        album:psz_album
228                                    andArtUrl:psz_arturl];
229
230     free( psz_title );
231     free( psz_artist );
232     free( psz_album );
233     free( psz_arturl );
234     return VLC_SUCCESS;
235 }
236
237 /*****************************************************************************
238  * VLCGrowlDelegate
239  *****************************************************************************/
240 @implementation VLCGrowlDelegate
241
242 - (id)initWithInterfaceThread:(intf_thread_t *)thread {
243     if( !( self = [super init] ) )
244         return nil;
245
246     @autoreleasepool {
247         // Subscribe to notifications to determine if VLC is in foreground or not
248         [[NSNotificationCenter defaultCenter] addObserver:self
249                                                  selector:@selector(applicationActiveChange:)
250                                                      name:NSApplicationDidBecomeActiveNotification
251                                                    object:nil];
252
253         [[NSNotificationCenter defaultCenter] addObserver:self
254                                                  selector:@selector(applicationActiveChange:)
255                                                      name:NSApplicationDidResignActiveNotification
256                                                    object:nil];
257     }
258     // Start in background
259     isInForeground = NO;
260
261     // Check for native notification support
262     Class userNotificationClass = NSClassFromString(@"NSUserNotification");
263     Class userNotificationCenterClass = NSClassFromString(@"NSUserNotificationCenter");
264     hasNativeNotifications = (userNotificationClass && userNotificationCenterClass) ? YES : NO;
265
266     lastNotification = nil;
267     applicationName = nil;
268     notificationType = nil;
269     registrationDictionary = nil;
270     interfaceThread = thread;
271
272     return self;
273 }
274
275 - (void)dealloc
276 {
277 #pragma clang diagnostic push
278 #pragma clang diagnostic ignored "-Wpartial-availability"
279 #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1080
280     // Clear the remaining lastNotification in Notification Center, if any
281     @autoreleasepool {
282         if (lastNotification && hasNativeNotifications) {
283             [NSUserNotificationCenter.defaultUserNotificationCenter
284              removeDeliveredNotification:(NSUserNotification *)lastNotification];
285             [lastNotification release];
286         }
287         [[NSNotificationCenter defaultCenter] removeObserver:self];
288     }
289 #endif
290 #pragma clang diagnostic pop
291
292     // Release everything
293     [applicationName release];
294     [notificationType release];
295     [registrationDictionary release];
296     [super dealloc];
297 }
298
299 - (void)registerToGrowl
300 {
301     @autoreleasepool {
302         applicationName = [[NSString alloc] initWithUTF8String:_( "VLC media player" )];
303         notificationType = [[NSString alloc] initWithUTF8String:_( "New input playing" )];
304
305         NSArray *defaultAndAllNotifications = [NSArray arrayWithObject: notificationType];
306         registrationDictionary = [[NSMutableDictionary alloc] init];
307         [registrationDictionary setObject:defaultAndAllNotifications
308                                    forKey:GROWL_NOTIFICATIONS_ALL];
309         [registrationDictionary setObject:defaultAndAllNotifications
310                                    forKey: GROWL_NOTIFICATIONS_DEFAULT];
311
312         [GrowlApplicationBridge setGrowlDelegate:self];
313
314 #pragma clang diagnostic push
315 #pragma clang diagnostic ignored "-Wpartial-availability"
316 #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1080
317         if (hasNativeNotifications) {
318             [[NSUserNotificationCenter defaultUserNotificationCenter]
319              setDelegate:(id<NSUserNotificationCenterDelegate>)self];
320         }
321 #endif
322 #pragma clang diagnostic pop
323     }
324 }
325
326 - (void)notifyWithTitle:(const char *)title
327                  artist:(const char *)artist
328                   album:(const char *)album
329               andArtUrl:(const char *)url
330 {
331     @autoreleasepool {
332         // Do not notify if in foreground
333         if (isInForeground)
334             return;
335
336         // Init Cover
337         NSData *coverImageData = nil;
338         NSImage *coverImage = nil;
339
340         if (url) {
341             coverImageData = [NSData dataWithContentsOfFile:[NSString stringWithUTF8String:url]];
342             coverImage = [[NSImage alloc] initWithData:coverImageData];
343         }
344
345         // Init Track info
346         NSString *titleStr = nil;
347         NSString *artistStr = nil;
348         NSString *albumStr = nil;
349
350         if (title) {
351             titleStr = [NSString stringWithUTF8String:title];
352         } else {
353             // Without title, notification makes no sense, so return here
354             // title should never be empty, but better check than crash.
355             [coverImage release];
356             return;
357         }
358         if (artist)
359             artistStr = [NSString stringWithUTF8String:artist];
360         if (album)
361             albumStr = [NSString stringWithUTF8String:album];
362
363         // Notification stuff
364         if ([GrowlApplicationBridge isGrowlRunning]) {
365             // Make the Growl notification string
366             NSString *desc = nil;
367
368             if (artistStr && albumStr) {
369                 desc = [NSString stringWithFormat:@"%@\n%@ [%@]", titleStr, artistStr, albumStr];
370             } else if (artistStr) {
371                 desc = [NSString stringWithFormat:@"%@\n%@", titleStr, artistStr];
372             } else {
373                 desc = titleStr;
374             }
375
376             // Send notification
377             [GrowlApplicationBridge notifyWithTitle:[NSString stringWithUTF8String:_("Now playing")]
378                                         description:desc
379                                    notificationName:notificationType
380                                            iconData:coverImageData
381                                            priority:0
382                                            isSticky:NO
383                                        clickContext:nil
384                                          identifier:@"VLCNowPlayingNotification"];
385         } else if (hasNativeNotifications) {
386 #pragma clang diagnostic push
387 #pragma clang diagnostic ignored "-Wpartial-availability"
388 #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1080
389             // Make the OS X notification and string
390             NSUserNotification *notification = [NSUserNotification new];
391             NSString *desc = nil;
392
393             if (artistStr && albumStr) {
394                 desc = [NSString stringWithFormat:@"%@ – %@", artistStr, albumStr];
395             } else if (artistStr) {
396                 desc = artistStr;
397             }
398
399             notification.title              = titleStr;
400             notification.subtitle           = desc;
401             notification.hasActionButton    = YES;
402             notification.actionButtonTitle  = [NSString stringWithUTF8String:_("Skip")];
403
404             // Private APIs to set cover image, see rdar://23148801
405             // and show action button, see rdar://23148733
406             [notification setValue:coverImage forKey:@"_identityImage"];
407             [notification setValue:@(YES) forKey:@"_showsButtons"];
408             [NSUserNotificationCenter.defaultUserNotificationCenter deliverNotification:notification];
409             [notification release];
410 #endif
411 #pragma clang diagnostic pop
412         }
413
414         // Release stuff
415         [coverImage release];
416     }
417 }
418
419 /*****************************************************************************
420  * Delegate methods
421  *****************************************************************************/
422 - (NSDictionary *)registrationDictionaryForGrowl
423 {
424     return registrationDictionary;
425 }
426
427 - (NSString *)applicationNameForGrowl
428 {
429     return applicationName;
430 }
431
432 - (void)applicationActiveChange:(NSNotification *)n {
433     if (n.name == NSApplicationDidBecomeActiveNotification)
434         isInForeground = YES;
435     else if (n.name == NSApplicationDidResignActiveNotification)
436         isInForeground = NO;
437 }
438
439 #pragma clang diagnostic push
440 #pragma clang diagnostic ignored "-Wpartial-availability"
441 #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1080
442 - (void)userNotificationCenter:(NSUserNotificationCenter *)center
443        didActivateNotification:(NSUserNotification *)notification
444 {
445     // Skip to next song
446     if (notification.activationType == NSUserNotificationActivationTypeActionButtonClicked) {
447         playlist_Next(pl_Get(interfaceThread));
448     }
449 }
450
451 - (void)userNotificationCenter:(NSUserNotificationCenter *)center
452         didDeliverNotification:(NSUserNotification *)notification
453 {
454     // Only keep the most recent notification in the Notification Center
455     if (lastNotification) {
456         [center removeDeliveredNotification: (NSUserNotification *)lastNotification];
457         [lastNotification release];
458     }
459     [notification retain];
460     lastNotification = notification;
461 }
462 #endif
463 #pragma clang diagnostic pop
464 @end