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