osx_notifications.m 16.5 KB
Newer Older
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 8 9
 * $Id$
 *
 * Authors: Rafaël Carré <funman@videolanorg>
10
 *          Felix Paul Kühne <fkuehne@videolan.org
11
 *          Marvin Scholz <epirat07@gmail.com>
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
 *
27 28
 * ---
 *
29 30 31 32 33
 * Growl specific code, ripped from growlnotify:
 *
 * Copyright (c) The Growl Project, 2004-2005
 * All rights reserved.
 *
34 35
 * Redistribution and use in source and binary forms, with or without modification,
 * 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 41
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
42
 * 3. Neither the name of Growl nor the names of its contributors
43 44
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
45
 *
46 47 48 49 50 51 52 53 54 55
 * 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.
56 57 58 59 60 61 62
 *
 *****************************************************************************/

/*****************************************************************************
 * Preamble
 *****************************************************************************/

63 64
#pragma clang diagnostic ignored "-Wunguarded-availability"

65 66 67 68
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

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 79
#include <vlc_meta.h>
#include <vlc_interface.h>
80
#include <vlc_url.h>
81 82

/*****************************************************************************
83
 * intf_sys_t, VLCGrowlDelegate
84
 *****************************************************************************/
85 86
@interface VLCGrowlDelegate : NSObject <GrowlApplicationBridgeDelegate>
{
87 88 89 90
    NSString *applicationName;
    NSString *notificationType;
    NSMutableDictionary *registrationDictionary;
    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 99 100 101
- (void)notifyWithTitle:(const char *)title
                 artist:(const char *)artist
                  album:(const char *)album
              andArtUrl:(const char *)url;
102 103
@end

104 105
struct intf_sys_t
{
106
    VLCGrowlDelegate *o_growl_delegate;
107 108 109 110 111 112 113 114
};

/*****************************************************************************
 * Local prototypes
 *****************************************************************************/
static int  Open    ( vlc_object_t * );
static void Close   ( vlc_object_t * );

115
static int InputCurrent( vlc_object_t *, const char *,
116
                      vlc_value_t, vlc_value_t, void * );
117 118 119 120

/*****************************************************************************
 * Module descriptor
 ****************************************************************************/
121
vlc_module_begin ()
122 123
set_category( CAT_INTERFACE )
set_subcategory( SUBCAT_INTERFACE_CONTROL )
124 125 126
set_shortname( "OSX-Notifications" )
add_shortcut( "growl" )
set_description( N_("OS X Notification Plugin") )
127 128
set_capability( "interface", 0 )
set_callbacks( Open, Close )
129
vlc_module_end ()
130 131 132 133 134 135 136

/*****************************************************************************
 * Open: initialize and create stuff
 *****************************************************************************/
static int Open( vlc_object_t *p_this )
{
    intf_thread_t *p_intf = (intf_thread_t *)p_this;
137 138
    playlist_t *p_playlist = pl_Get( p_intf );
    intf_sys_t *p_sys = p_intf->p_sys = calloc( 1, sizeof(intf_sys_t) );
139

140 141
    if( !p_sys )
        return VLC_ENOMEM;
142

143
    p_sys->o_growl_delegate = [[VLCGrowlDelegate alloc] initWithInterfaceThread:p_intf];
144
    if( !p_sys->o_growl_delegate )
145 146 147 148 149
    {
        free( p_sys );
        return VLC_ENOMEM;
    }

150
    var_AddCallback( p_playlist, "input-current", InputCurrent, p_intf );
151

152
    [p_sys->o_growl_delegate registerToGrowl];
153 154 155 156 157 158 159 160
    return VLC_SUCCESS;
}

/*****************************************************************************
 * Close: destroy interface stuff
 *****************************************************************************/
static void Close( vlc_object_t *p_this )
{
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 170 171 172
    free( p_sys );
}

/*****************************************************************************
173
 * InputCurrent: Current playlist item changed callback
174
 *****************************************************************************/
175 176
static int InputCurrent( vlc_object_t *p_this, const char *psz_var,
                        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 183 184 185 186
    input_thread_t *p_input = newval.p_address;
    char *psz_title = NULL;
    char *psz_artist = NULL;
    char *psz_album = NULL;
    char *psz_arturl = NULL;
187

188
    if( !p_input )
189
        return VLC_SUCCESS;
190

191 192
    input_item_t *p_item = input_GetItem( p_input );
    if( !p_item )
193 194
        return VLC_SUCCESS;

195 196 197
    /* Get title */
    psz_title = input_item_GetNowPlayingFb( p_item );
    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 209 210 211
    if( EMPTY_STR( psz_artist ) )
        FREENULL( psz_artist );

    /* Get Album name */
212
    psz_album = input_item_GetAlbum( p_item ) ;
213 214
    if( EMPTY_STR( psz_album ) )
        FREENULL( psz_album );
215

216 217
    /* Get Art path */
    psz_arturl = input_item_GetArtURL( p_item );
218 219
    if( psz_arturl )
    {
220
        char *psz = vlc_uri2path( psz_arturl );
221 222 223
        free( psz_arturl );
        psz_arturl = psz;
    }
224

225 226 227 228
    [p_sys->o_growl_delegate notifyWithTitle:psz_title
                                      artist:psz_artist
                                       album:psz_album
                                   andArtUrl:psz_arturl];
229

230 231 232 233 234 235 236 237
    free( psz_title );
    free( psz_artist );
    free( psz_album );
    free( psz_arturl );
    return VLC_SUCCESS;
}

/*****************************************************************************
238
 * VLCGrowlDelegate
239
 *****************************************************************************/
240 241
@implementation VLCGrowlDelegate

242
- (id)initWithInterfaceThread:(intf_thread_t *)thread {
243 244 245
    if( !( self = [super init] ) )
        return nil;

246 247 248 249 250 251 252 253 254 255 256 257 258 259 260
    @autoreleasepool {
        // Subscribe to notifications to determine if VLC is in foreground or not
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(applicationActiveChange:)
                                                     name:NSApplicationDidBecomeActiveNotification
                                                   object:nil];

        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(applicationActiveChange:)
                                                     name:NSApplicationDidResignActiveNotification
                                                   object:nil];
    }
    // Start in background
    isInForeground = NO;

261 262 263 264 265 266
    // Check for native notification support
    Class userNotificationClass = NSClassFromString(@"NSUserNotification");
    Class userNotificationCenterClass = NSClassFromString(@"NSUserNotificationCenter");
    hasNativeNotifications = (userNotificationClass && userNotificationCenterClass) ? YES : NO;

    lastNotification = nil;
267 268 269 270 271
    applicationName = nil;
    notificationType = nil;
    registrationDictionary = nil;
    interfaceThread = thread;

272
    return self;
273 274
}

275
- (void)dealloc
276
{
277 278
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wpartial-availability"
279 280 281
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1080
    // Clear the remaining lastNotification in Notification Center, if any
    @autoreleasepool {
282
        if (lastNotification && hasNativeNotifications) {
283 284 285 286
            [NSUserNotificationCenter.defaultUserNotificationCenter
             removeDeliveredNotification:(NSUserNotification *)lastNotification];
            [lastNotification release];
        }
287
        [[NSNotificationCenter defaultCenter] removeObserver:self];
288 289
    }
#endif
290
#pragma clang diagnostic pop
291 292 293 294 295

    // Release everything
    [applicationName release];
    [notificationType release];
    [registrationDictionary release];
296 297
    [super dealloc];
}
298

299 300
- (void)registerToGrowl
{
301
    @autoreleasepool {
302 303 304 305 306 307 308 309 310
        applicationName = [[NSString alloc] initWithUTF8String:_( "VLC media player" )];
        notificationType = [[NSString alloc] initWithUTF8String:_( "New input playing" )];

        NSArray *defaultAndAllNotifications = [NSArray arrayWithObject: notificationType];
        registrationDictionary = [[NSMutableDictionary alloc] init];
        [registrationDictionary setObject:defaultAndAllNotifications
                                   forKey:GROWL_NOTIFICATIONS_ALL];
        [registrationDictionary setObject:defaultAndAllNotifications
                                   forKey: GROWL_NOTIFICATIONS_DEFAULT];
311

312
        [GrowlApplicationBridge setGrowlDelegate:self];
313

314 315
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wpartial-availability"
316
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1080
317 318 319 320
        if (hasNativeNotifications) {
            [[NSUserNotificationCenter defaultUserNotificationCenter]
             setDelegate:(id<NSUserNotificationCenterDelegate>)self];
        }
321
#endif
322
#pragma clang diagnostic pop
323
    }
324
}
325

326 327 328 329
- (void)notifyWithTitle:(const char *)title
                 artist:(const char *)artist
                  album:(const char *)album
              andArtUrl:(const char *)url
330
{
331
    @autoreleasepool {
332
        // Do not notify if in foreground
333
        if (isInForeground)
334 335
            return;

336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354
        // Init Cover
        NSData *coverImageData = nil;
        NSImage *coverImage = nil;

        if (url) {
            coverImageData = [NSData dataWithContentsOfFile:[NSString stringWithUTF8String:url]];
            coverImage = [[NSImage alloc] initWithData:coverImageData];
        }

        // Init Track info
        NSString *titleStr = nil;
        NSString *artistStr = nil;
        NSString *albumStr = nil;

        if (title) {
            titleStr = [NSString stringWithUTF8String:title];
        } else {
            // Without title, notification makes no sense, so return here
            // title should never be empty, but better check than crash.
355
            [coverImage release];
356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382
            return;
        }
        if (artist)
            artistStr = [NSString stringWithUTF8String:artist];
        if (album)
            albumStr = [NSString stringWithUTF8String:album];

        // Notification stuff
        if ([GrowlApplicationBridge isGrowlRunning]) {
            // Make the Growl notification string
            NSString *desc = nil;

            if (artistStr && albumStr) {
                desc = [NSString stringWithFormat:@"%@\n%@ [%@]", titleStr, artistStr, albumStr];
            } else if (artistStr) {
                desc = [NSString stringWithFormat:@"%@\n%@", titleStr, artistStr];
            } else {
                desc = titleStr;
            }

            // Send notification
            [GrowlApplicationBridge notifyWithTitle:[NSString stringWithUTF8String:_("Now playing")]
                                        description:desc
                                   notificationName:notificationType
                                           iconData:coverImageData
                                           priority:0
                                           isSticky:NO
383 384
                                       clickContext:nil
                                         identifier:@"VLCNowPlayingNotification"];
385
        } else if (hasNativeNotifications) {
386 387
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wpartial-availability"
388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1080
            // Make the OS X notification and string
            NSUserNotification *notification = [NSUserNotification new];
            NSString *desc = nil;

            if (artistStr && albumStr) {
                desc = [NSString stringWithFormat:@"%@ – %@", artistStr, albumStr];
            } else if (artistStr) {
                desc = artistStr;
            }

            notification.title              = titleStr;
            notification.subtitle           = desc;
            notification.hasActionButton    = YES;
            notification.actionButtonTitle  = [NSString stringWithUTF8String:_("Skip")];

            // Private APIs to set cover image, see rdar://23148801
            // and show action button, see rdar://23148733
            [notification setValue:coverImage forKey:@"_identityImage"];
            [notification setValue:@(YES) forKey:@"_showsButtons"];
            [NSUserNotificationCenter.defaultUserNotificationCenter deliverNotification:notification];
            [notification release];
#endif
411
#pragma clang diagnostic pop
412 413 414 415
        }

        // Release stuff
        [coverImage release];
416
    }
417 418
}

419 420 421 422
/*****************************************************************************
 * Delegate methods
 *****************************************************************************/
- (NSDictionary *)registrationDictionaryForGrowl
423
{
424
    return registrationDictionary;
425 426
}

427 428
- (NSString *)applicationNameForGrowl
{
429 430 431
    return applicationName;
}

432 433 434 435 436 437 438
- (void)applicationActiveChange:(NSNotification *)n {
    if (n.name == NSApplicationDidBecomeActiveNotification)
        isInForeground = YES;
    else if (n.name == NSApplicationDidResignActiveNotification)
        isInForeground = NO;
}

439 440
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wpartial-availability"
441 442 443 444
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1080
- (void)userNotificationCenter:(NSUserNotificationCenter *)center
       didActivateNotification:(NSUserNotification *)notification
{
445
    // Skip to next song
446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462
    if (notification.activationType == NSUserNotificationActivationTypeActionButtonClicked) {
        playlist_Next(pl_Get(interfaceThread));
    }
}

- (void)userNotificationCenter:(NSUserNotificationCenter *)center
        didDeliverNotification:(NSUserNotification *)notification
{
    // Only keep the most recent notification in the Notification Center
    if (lastNotification) {
        [center removeDeliveredNotification: (NSUserNotification *)lastNotification];
        [lastNotification release];
    }
    [notification retain];
    lastNotification = notification;
}
#endif
463
#pragma clang diagnostic pop
464
@end