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: e4b8f516ab93db2e93cc23a7a0d89fb351be8fe8 $
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
104struct intf_sys_t
105{
106    VLCGrowlDelegate *o_growl_delegate;
107};
108
109/*****************************************************************************
110 * Local prototypes
111 *****************************************************************************/
112static int  Open    ( vlc_object_t * );
113static void Close   ( vlc_object_t * );
114
115static int InputCurrent( vlc_object_t *, const char *,
116                      vlc_value_t, vlc_value_t, void * );
117
118/*****************************************************************************
119 * Module descriptor
120 ****************************************************************************/
121vlc_module_begin ()
122set_category( CAT_INTERFACE )
123set_subcategory( SUBCAT_INTERFACE_CONTROL )
124set_shortname( "OSX-Notifications" )
125add_shortcut( "growl" )
126set_description( N_("OS X Notification Plugin") )
127set_capability( "interface", 0 )
128set_callbacks( Open, Close )
129vlc_module_end ()
130
131/*****************************************************************************
132 * Open: initialize and create stuff
133 *****************************************************************************/
134static 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 *****************************************************************************/
159static 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 *****************************************************************************/
175static 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
465