From 0eb891fc9c85b4bf6feb7741d4d6987e654d3bfd Mon Sep 17 00:00:00 2001 From: Kevin Walzer Date: Tue, 1 Jun 2021 02:20:57 +0000 Subject: Re-work macOS sysnotify implementation to remove code-signing requirement and multiple API's --- doc/sysnotify.n | 21 +-- macosx/tkMacOSXSysTray.c | 410 ++--------------------------------------------- 2 files changed, 13 insertions(+), 418 deletions(-) diff --git a/doc/sysnotify.n b/doc/sysnotify.n index 8951cf0..e375c7a 100644 --- a/doc/sysnotify.n +++ b/doc/sysnotify.n @@ -32,25 +32,8 @@ accompany the text. .TP \fBmacOS\fR . -The macOS version embeds two separate under-the-hood implementations -using different notification APIs. The choice of which one to use -depends on which version of the OS is being run and the state of the -Tk application code. The newer API, introduced in macOS 10.14, -requires that the application accessing the API be code-signed, or the -notification will not display. (A self-signed certificate seems to be -sufficient.) The older API was deprecated but not removed in macOS -11.0. Tk uses the newer API only for signed applications running on -macOS 10.14 or newer. Otherwise it falls back to the older API. A -quirk which developers should be aware of is that if an unsigned -version of Wish (or an application derived from it) is installed on -top of a signed version after the signed version has been registered -with System Preferences then neither API will be allowed to show -notifications, making Tk's automatic fallback to the older API -ineffective. To re-enable notifications the application must be -deleted from Apple's System Preferences Notifications section. (There -is no removal button, so this is done by selecting the application and -pressing the Delete key.) -. +The macOS version will request permission from the user to authorize +notifications. This must be activated in Apple's System Preferences Notifications section. .TP \fBWindows\fR . diff --git a/macosx/tkMacOSXSysTray.c b/macosx/tkMacOSXSysTray.c index a0f0829..d2ecfaf 100644 --- a/macosx/tkMacOSXSysTray.c +++ b/macosx/tkMacOSXSysTray.c @@ -19,71 +19,6 @@ #include "tkMacOSXPrivate.h" /* - * Prior to macOS 10.14 user notifications were handled by the NSApplication's - * NSUserNotificationCenter via a NSUserNotificationCenterDelegate object. - * These classes were defined in the CoreFoundation framework. In macOS 10.14 - * a separate UserNotifications framework was introduced which adds some - * additional features, including custom controls on the notification window - * but primarily a requirement that an application must be authorized before - * being allowed to post a notification. This framework uses a different - * class, the UNUserNotificationCenter, and its delegate follows a different - * protocol, named UNUserNotificationCenterDelegate. - * - * In macOS 11.0 the NSUserNotificationCenter and its delegate protocol were - * deprecated. To make matters more complicated, it turns out that there is a - * secret undocumented additional requirement that an app which is not signed - * can never be authorized to send notifications via the UNNotificationCenter. - * (As of 11.0, it appears that it is sufficient to sign the app with a - * self-signed certificate, however.) - * - * The workaround implemented here is to define two classes, TkNSNotifier and - * TkUNNotifier, each of which provides one of these protocols on macOS 10.14 - * and newer. If the TkUSNotifier is able to obtain authorization it is used. - * Otherwise, TkNSNotifier is used. Building TkNSNotifier on 11.0 or later - * produces deprecation warnings which are suppressed by enclosing the - * interface and implementation in #pragma blocks. The first time that the tk - * systray command in initialized in an interpreter an attempt is made to - * obtain authorization for sending notifications with the UNNotificationCenter - * on systems and the result is saved in a static variable. - */ - -//#define DEBUG -#ifdef DEBUG - -/* - * This macro uses the do ... while(0) trick to swallow semicolons. It logs to - * a temp file because apps launched from an icon have no stdout or stderr and - * because NSLog has a tendency to not produce any console messages at certain - * stages of launching an app. - */ - -#define DEBUG_LOG(format, ...) \ - do { \ - FILE* logfile = fopen("/tmp/tklog", "a"); \ - fprintf(logfile, format, ##__VA_ARGS__); \ - fflush(logfile); \ - fclose(logfile); } while (0) -#else -#define DEBUG_LOG(format, ...) -#endif - -#define BUILD_TARGET_HAS_NOTIFICATION (MAC_OS_X_VERSION_MAX_ALLOWED >= 101000) -#define BUILD_TARGET_HAS_UN_FRAMEWORK (MAC_OS_X_VERSION_MAX_ALLOWED >= 101400) -#if MAC_OS_X_VERSION_MAX_ALLOWED > 101500 -#define ALERT_OPTION UNNotificationPresentationOptionList | \ - UNNotificationPresentationOptionBanner -#else -#define ALERT_OPTION UNNotificationPresentationOptionAlert -#endif - -#if BUILD_TARGET_HAS_UN_FRAMEWORK -#import -static NSString *TkNotificationCategory; -#endif - -#if BUILD_TARGET_HAS_NOTIFICATION - -/* * Class declaration for TkStatusItem. */ @@ -107,97 +42,7 @@ static NSString *TkNotificationCategory; @end -/* - * Class declaration for TkNSNotifier. A TkNSNotifier object has no attributes - * but implements the NSUserNotificationCenterDelegate protocol. It also has - * one additional method which posts a user notification. There is one - * TkNSNotifier for the application, shared by all interpreters. - */ - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" -@interface TkNSNotifier: NSObject { -} - -/* - * Post a notification. - */ - -- (void) postNotificationWithTitle : (NSString *) title message: (NSString *) detail; - -/* - * The following methods comprise the NSUserNotificationCenterDelegate protocol. - */ - -- (void) userNotificationCenter:(NSUserNotificationCenter *)center - didDeliverNotification:(NSUserNotification *)notification; - -- (void) userNotificationCenter:(NSUserNotificationCenter *)center - didActivateNotification:(NSUserNotification *)notification; - -- (BOOL) userNotificationCenter:(NSUserNotificationCenter *)center - shouldPresentNotification:(NSUserNotification *)notification; - -@end -#pragma clang diagnostic pop - -/* - * The singleton instance of TkNSNotifier shared by all interpreters in this - * application. - */ - -static TkNSNotifier *NSnotifier = nil; -#if BUILD_TARGET_HAS_UN_FRAMEWORK - -/* - * Class declaration for TkUNNotifier. A TkUNNotifier object has no attributes - * but implements the UNUserNotificationCenterDelegate protocol It also has two - * additional methods. One requests authorization to post notification via the - * UserNotification framework and the other posts a user notification. There is - * at most one TkUNNotifier for the application, shared by all interpreters. - */ - -@interface TkUNNotifier: NSObject { -} - - /* - * Request authorization to post a notification. - */ - -- (void) requestAuthorization; - -/* - * Post a notification. - */ - -- (void) postNotificationWithTitle : (NSString *) title message: (NSString *) detail; - -/* - * The following methods comprise the UNNotificationCenterDelegate protocol: - */ - -- (void)userNotificationCenter:(UNUserNotificationCenter *)center - didReceiveNotificationResponse:(UNNotificationResponse *)response - withCompletionHandler:(void (^)(void))completionHandler; - -- (void)userNotificationCenter:(UNUserNotificationCenter *)center - willPresentNotification:(UNNotification *)notification - withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler; - -- (void)userNotificationCenter:(UNUserNotificationCenter *)center - openSettingsForNotification:(UNNotification *)notification; - -@end - -/* - * The singleton instance of TkUNNotifier shared by all interpeters is stored - * in this static variable. - */ - -static TkUNNotifier *UNnotifier = nil; - -#endif /* * Class declaration for TkStatusItem. A TkStatusItem represents an icon posted @@ -297,163 +142,7 @@ static TkUNNotifier *UNnotifier = nil; typedef TkStatusItem** StatusItemInfo; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" -@implementation TkNSNotifier : NSObject -- (void) postNotificationWithTitle : (NSString * ) title - message: (NSString * ) detail -{ - NSUserNotification *notification; - NSUserNotificationCenter *center; - - center = [NSUserNotificationCenter defaultUserNotificationCenter]; - notification = [[NSUserNotification alloc] init]; - notification.title = title; - notification.informativeText = detail; - notification.soundName = NSUserNotificationDefaultSoundName; - DEBUG_LOG("Sending NSNotification.\n"); - [center deliverNotification:notification]; -} - -/* - * Implementation of the NSUserNotificationDelegate protocol. - */ - -- (BOOL) userNotificationCenter: (NSUserNotificationCenter *) center - shouldPresentNotification: (NSUserNotification *)notification -{ - (void) center; - (void) notification; - - return YES; -} - -- (void) userNotificationCenter:(NSUserNotificationCenter *)center - didDeliverNotification:(NSUserNotification *)notification -{ - (void) center; - (void) notification; -} - -- (void) userNotificationCenter:(NSUserNotificationCenter *)center - didActivateNotification:(NSUserNotification *)notification -{ - (void) center; - (void) notification; -} - -@end -#pragma clang diagnostic pop - -/* - * Static variable which records whether the app is authorized to send - * notifications via the UNUserNotificationCenter. - */ - -#if BUILD_TARGET_HAS_UN_FRAMEWORK - -@implementation TkUNNotifier : NSObject - -- (void) requestAuthorization -{ - UNUserNotificationCenter *center; - UNAuthorizationOptions options = UNAuthorizationOptionAlert | - UNAuthorizationOptionSound | - UNAuthorizationOptionBadge | - UNAuthorizationOptionProvidesAppNotificationSettings; - if (![NSApp isSigned]) { - - /* - * No point in even asking. - */ - - DEBUG_LOG("Unsigned app: UNUserNotifications are not available.\n"); - return; - } - - center = [UNUserNotificationCenter currentNotificationCenter]; - [center requestAuthorizationWithOptions: options - completionHandler: ^(BOOL granted, NSError* error) - { - if (error || granted == NO) { - DEBUG_LOG("Authorization for UNUserNotifications denied\n"); - } - }]; -} - -- (void) postNotificationWithTitle: (NSString * ) title - message: (NSString * ) detail -{ - UNUserNotificationCenter *center; - UNMutableNotificationContent* content; - UNNotificationRequest *request; - center = [UNUserNotificationCenter currentNotificationCenter]; - center.delegate = (id) self; - content = [[UNMutableNotificationContent alloc] init]; - content.title = title; - content.body = detail; - content.sound = [UNNotificationSound defaultSound]; - content.categoryIdentifier = TkNotificationCategory; - request = [UNNotificationRequest - requestWithIdentifier:[[NSUUID UUID] UUIDString] - content:content - trigger:nil - ]; - [center addNotificationRequest: request - withCompletionHandler: ^(NSError* error) { - if (error) { - DEBUG_LOG("addNotificationRequest: error = %s\n", \ - [NSString stringWithFormat:@"%@", \ - error.userInfo].UTF8String); - } - }]; -} - -/* - * Implementation of the UNUserNotificationDelegate protocol. - */ - -- (void) userNotificationCenter:(UNUserNotificationCenter *)center - didReceiveNotificationResponse:(UNNotificationResponse *)response - withCompletionHandler:(void (^)(void))completionHandler -{ - /* - * Called when the user dismisses a notification. - */ - - DEBUG_LOG("didReceiveNotification\n"); - completionHandler(); -} - -- (void) userNotificationCenter:(UNUserNotificationCenter *)center - willPresentNotification:(UNNotification *)notification - withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler -{ - - /* - * This is called before presenting a notification, even when the user has - * turned off notifications. - */ - - DEBUG_LOG("willPresentNotification\n"); -#if MAC_OS_X_VERSION_MAX_ALLOWED >= 101400 - if (@available(macOS 11.0, *)) { - completionHandler(ALERT_OPTION); - } -#endif -} - -- (void) userNotificationCenter:(UNUserNotificationCenter *)center - openSettingsForNotification:(UNNotification *)notification -{ - DEBUG_LOG("openSettingsForNotification\n"); - // Does something need to be done here? -} - -@end - -#endif /* *---------------------------------------------------------------------- @@ -729,47 +418,20 @@ static int SysNotifyObjCmd( NSString *title = [NSString stringWithUTF8String: Tcl_GetString(objv[1])]; NSString *message = [NSString stringWithUTF8String: Tcl_GetString(objv[2])]; - - /* - * Update the authorization status in case the user enabled or disabled - * notifications after the app started up. - */ - -#if BUILD_TARGET_HAS_UN_FRAMEWORK - - if (UNnotifier && [NSApp isSigned]) { - UNUserNotificationCenter *center; - - center = [UNUserNotificationCenter currentNotificationCenter]; - [center getNotificationSettingsWithCompletionHandler: - ^(UNNotificationSettings *settings) - { -#if !defined(DEBUG) - (void) settings; -#endif - DEBUG_LOG("Reported authorization status is %ld\n", - settings.authorizationStatus); - }]; - } - -#endif - - if ([NSApp macOSVersion] < 101400 || ![NSApp isSigned]) { - DEBUG_LOG("Using the NSUserNotificationCenter\n"); - [NSnotifier postNotificationWithTitle : title message: message]; - } else { - -#if BUILD_TARGET_HAS_UN_FRAMEWORK - - DEBUG_LOG("Using the UNUserNotificationCenter\n"); - [UNnotifier postNotificationWithTitle : title message: message]; -#endif - } + NSMutableString *notify = [NSMutableString new]; + [notify appendString: @"display notification "]; + [notify appendString:@"\""]; + [notify appendString:message]; + [notify appendString:@"\""]; + [notify appendString:@" with title \""]; + [notify appendString:title]; + [notify appendString:@"\""]; + NSAppleScript *scpt = [[[NSAppleScript alloc] initWithSource:notify] autorelease]; + NSAppleEventDescriptor *result = [scpt executeAndReturnError:nil]; return TCL_OK; } -#endif // if BUILD_TARGET_HAS_NOTIFICATION /* *---------------------------------------------------------------------- @@ -791,73 +453,23 @@ static int SysNotifyObjCmd( *---------------------------------------------------------------------- */ -#if BUILD_TARGET_HAS_NOTIFICATION - int MacSystrayInit(Tcl_Interp *interp) { /* - * Initialize the TkStatusItem for this interpreter and, if necessary, - * the shared TkNSNotifier and TkUNNotifier. + * Initialize the TkStatusItem for this interpreter. */ StatusItemInfo info = (StatusItemInfo) ckalloc(sizeof(StatusItemInfo)); *info = 0; - if (NSnotifier == nil) { - NSnotifier = [[TkNSNotifier alloc] init]; - } - -#if BUILD_TARGET_HAS_UN_FRAMEWORK - - if (@available(macOS 10.14, *)) { - UNUserNotificationCenter *center; - UNNotificationCategory *category; - NSSet *categories; - - if (UNnotifier == nil) { - UNnotifier = [[TkUNNotifier alloc] init]; - - /* - * Request authorization to use the UserNotification framework. If - * the app code is signed and there are no notification preferences - * settings for this app, a dialog will be opened to prompt the - * user to choose settings. Note that the request is asynchronous, - * so even if the preferences setting exists the result is not - * available immediately. - */ - - [UNnotifier requestAuthorization]; - } - TkNotificationCategory = @"Basic Tk Notification"; - center = [UNUserNotificationCenter currentNotificationCenter]; - center = [UNUserNotificationCenter currentNotificationCenter]; - category = [UNNotificationCategory - categoryWithIdentifier:TkNotificationCategory - actions:@[] - intentIdentifiers:@[] - options: UNNotificationCategoryOptionNone]; - categories = [NSSet setWithObjects:category, nil]; - [center setNotificationCategories: categories]; - } -#endif - Tcl_CreateObjCommand(interp, "::tk::systray::_systray", MacSystrayObjCmd, info, (Tcl_CmdDeleteProc *)MacSystrayDestroy); Tcl_CreateObjCommand(interp, "::tk::sysnotify::_sysnotify", SysNotifyObjCmd, NULL, NULL); return TCL_OK; } -#else - -int -MacSystrayInit(TCL_UNUSED(Tcl_Interp *)) -{ - return TCL_OK; -} - -#endif // BUILD_TARGET_HAS_NOTIFICATION /* * Local Variables: -- cgit v0.12