2 // GrowlPreferencesController.m
5 // Created by Nelson Elhage on 8/24/04.
6 // Renamed from GrowlPreferences.m by Mac-arena the Bored Zo on 2005-06-27.
7 // Copyright 2004-2006 The Growl Project. All rights reserved.
9 // This file is under the BSD License, refer to License.txt for details
12 #import "GrowlPreferencesController.h"
13 #import "GrowlDefinesInternal.h"
14 #import "GrowlDefines.h"
15 #import "GrowlPathUtilities.h"
16 #import "NSStringAdditions.h"
17 #include "CFURLAdditions.h"
18 #include "CFDictionaryAdditions.h"
19 #include "LoginItemsAE.h"
20 #include <Security/SecKeychain.h>
21 #include <Security/SecKeychainItem.h>
23 #define keychainServiceName "Growl"
24 #define keychainAccountName "Growl"
26 CFTypeRef GrowlPreferencesController_objectForKey(CFTypeRef key) {
27 return [[GrowlPreferencesController sharedController] objectForKey:(id)key];
30 CFIndex GrowlPreferencesController_integerForKey(CFTypeRef key) {
31 Boolean keyExistsAndHasValidFormat;
32 return CFPreferencesGetAppIntegerValue((CFStringRef)key, (CFStringRef)GROWL_HELPERAPP_BUNDLE_IDENTIFIER, &keyExistsAndHasValidFormat);
35 Boolean GrowlPreferencesController_boolForKey(CFTypeRef key) {
36 Boolean keyExistsAndHasValidFormat;
37 return CFPreferencesGetAppBooleanValue((CFStringRef)key, (CFStringRef)GROWL_HELPERAPP_BUNDLE_IDENTIFIER, &keyExistsAndHasValidFormat);
40 unsigned short GrowlPreferencesController_unsignedShortForKey(CFTypeRef key)
42 CFIndex theIndex = GrowlPreferencesController_integerForKey(key);
44 if (theIndex > USHRT_MAX)
46 else if (theIndex < 0)
48 return (unsigned short)index;
51 @implementation GrowlPreferencesController
53 + (GrowlPreferencesController *) sharedController {
54 return [self sharedInstance];
57 - (id) initSingleton {
58 if ((self = [super initSingleton])) {
59 [[NSDistributedNotificationCenter defaultCenter] addObserver:self
60 selector:@selector(growlPreferencesChanged:)
61 name:GrowlPreferencesChanged
68 [[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
75 - (void) registerDefaults:(NSDictionary *)inDefaults {
76 NSUserDefaults *helperAppDefaults = [[NSUserDefaults alloc] init];
77 [helperAppDefaults addSuiteNamed:GROWL_HELPERAPP_BUNDLE_IDENTIFIER];
78 NSDictionary *existing = [helperAppDefaults persistentDomainForName:GROWL_HELPERAPP_BUNDLE_IDENTIFIER];
80 NSMutableDictionary *domain = [inDefaults mutableCopy];
81 [domain addEntriesFromDictionary:existing];
82 [helperAppDefaults setPersistentDomain:domain forName:GROWL_HELPERAPP_BUNDLE_IDENTIFIER];
85 [helperAppDefaults setPersistentDomain:inDefaults forName:GROWL_HELPERAPP_BUNDLE_IDENTIFIER];
87 [helperAppDefaults release];
88 SYNCHRONIZE_GROWL_PREFS();
91 - (id) objectForKey:(NSString *)key {
92 id value = (id)CFPreferencesCopyAppValue((CFStringRef)key, (CFStringRef)GROWL_HELPERAPP_BUNDLE_IDENTIFIER);
94 CFMakeCollectable(value);
95 return [value autorelease];
98 - (void) setObject:(id)object forKey:(NSString *)key {
99 CFPreferencesSetAppValue((CFStringRef)key,
100 (CFPropertyListRef)object,
101 (CFStringRef)GROWL_HELPERAPP_BUNDLE_IDENTIFIER);
103 SYNCHRONIZE_GROWL_PREFS();
106 CFNumberRef pidValue = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &pid);
107 CFStringRef pidKey = CFSTR("pid");
108 CFDictionaryRef userInfo = CFDictionaryCreate(kCFAllocatorDefault, (const void **)&pidKey, (const void **)&pidValue, 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
110 CFNotificationCenterPostNotification(CFNotificationCenterGetDistributedCenter(),
111 (CFStringRef)GrowlPreferencesChanged,
113 /*userInfo*/ userInfo,
114 /*deliverImmediately*/ false);
118 - (BOOL) boolForKey:(NSString *)key {
119 return GrowlPreferencesController_boolForKey((CFTypeRef)key);
122 - (void) setBool:(BOOL)value forKey:(NSString *)key {
123 NSNumber *object = [[NSNumber alloc] initWithBool:value];
124 [self setObject:object forKey:key];
128 - (CFIndex) integerForKey:(NSString *)key {
129 return GrowlPreferencesController_integerForKey((CFTypeRef)key);
132 - (void) setInteger:(CFIndex)value forKey:(NSString *)key {
134 NSNumber *object = [[NSNumber alloc] initWithInteger:value];
136 NSNumber *object = [[NSNumber alloc] initWithInt:value];
138 [self setObject:object forKey:key];
142 - (unsigned short)unsignedShortForKey:(NSString *)key
144 return GrowlPreferencesController_unsignedShortForKey((CFTypeRef)key);
148 - (void)setUnsignedShort:(unsigned short)theShort forKey:(NSString *)key
150 [self setObject:[NSNumber numberWithUnsignedShort:theShort] forKey:key];
153 - (void) synchronize {
154 SYNCHRONIZE_GROWL_PREFS();
158 #pragma mark Start-at-login control
160 - (BOOL) shouldStartGrowlAtLogin {
162 Boolean foundIt = false;
163 CFArrayRef loginItems = NULL;
165 //get the prefpane bundle and find GHA within it.
166 NSString *pathToGHA = [[NSBundle bundleWithIdentifier:GROWL_PREFPANE_BUNDLE_IDENTIFIER] pathForResource:@"GrowlHelperApp" ofType:@"app"];
167 //get the file url to GHA.
168 CFURLRef urlToGHA = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, (CFStringRef)pathToGHA, kCFURLPOSIXPathStyle, true);
170 status = LIAECopyLoginItems(&loginItems);
171 if (status == noErr) {
172 for (CFIndex i=0, count=CFArrayGetCount(loginItems); i<count; ++i) {
173 CFDictionaryRef loginItem = CFArrayGetValueAtIndex(loginItems, i);
174 foundIt = CFEqual(CFDictionaryGetValue(loginItem, kLIAEURL), urlToGHA);
178 CFRelease(loginItems);
186 - (void) setShouldStartGrowlAtLogin:(BOOL)flag {
187 //get the prefpane bundle and find GHA within it.
188 NSString *pathToGHA = [[NSBundle bundleWithIdentifier:GROWL_PREFPANE_BUNDLE_IDENTIFIER] pathForResource:@"GrowlHelperApp" ofType:@"app"];
189 [self setStartAtLogin:pathToGHA enabled:flag];
192 - (void) setStartAtLogin:(NSString *)path enabled:(BOOL)enabled {
194 CFArrayRef loginItems = NULL;
195 NSURL *url = [NSURL fileURLWithPath:path];
196 NSInteger existingLoginItemIndex = -1;
198 status = LIAECopyLoginItems(&loginItems);
200 if (status == noErr) {
201 NSEnumerator *enumerator = [(NSArray *)loginItems objectEnumerator];
202 NSDictionary *loginItemDict;
204 while ((loginItemDict = [enumerator nextObject])) {
205 if ([[loginItemDict objectForKey:(NSString *)kLIAEURL] isEqual:url]) {
206 existingLoginItemIndex = [(NSArray *)loginItems indexOfObjectIdenticalTo:loginItemDict];
212 if (enabled && (existingLoginItemIndex == -1))
213 LIAEAddURLAtEnd((CFURLRef)url, false);
214 else if (!enabled && (existingLoginItemIndex != -1))
215 LIAERemove(existingLoginItemIndex);
218 CFRelease(loginItems);
222 #pragma mark GrowlMenu running state
224 - (void) enableGrowlMenu {
225 NSBundle *bundle = [NSBundle bundleForClass:[GrowlPreferencesController class]];
226 NSString *growlMenuPath = [bundle pathForResource:@"GrowlMenu" ofType:@"app"];
227 NSURL *growlMenuURL = [NSURL fileURLWithPath:growlMenuPath];
228 [[NSWorkspace sharedWorkspace] openURLs:[NSArray arrayWithObject:growlMenuURL]
229 withAppBundleIdentifier:nil
230 options:NSWorkspaceLaunchWithoutAddingToRecents | NSWorkspaceLaunchWithoutActivation | NSWorkspaceLaunchAsync
231 additionalEventParamDescriptor:nil
232 launchIdentifiers:NULL];
235 - (void) disableGrowlMenu {
236 // Ask GrowlMenu to shutdown via the DNC
237 CFNotificationCenterPostNotification(CFNotificationCenterGetDistributedCenter(),
238 CFSTR("GrowlMenuShutdown"),
241 /*deliverImmediately*/ false);
245 #pragma mark Growl running state
247 - (void) setGrowlRunning:(BOOL)flag noMatterWhat:(BOOL)nmw {
248 // Store the desired running-state of the helper app for use by GHA.
249 [self setBool:flag forKey:GrowlEnabledKey];
251 //now launch or terminate as appropriate.
253 [self launchGrowl:nmw];
255 [self terminateGrowl];
258 - (BOOL) isRunning:(NSString *)theBundleIdentifier {
260 ProcessSerialNumber PSN = { kNoProcess, kNoProcess };
262 while (GetNextProcess(&PSN) == noErr) {
263 NSDictionary *infoDict = (NSDictionary *)ProcessInformationCopyDictionary(&PSN, kProcessDictionaryIncludeAllInformationMask);
265 NSString *bundleID = [infoDict objectForKey:(NSString *)kCFBundleIdentifierKey];
266 isRunning = bundleID && [bundleID isEqualToString:theBundleIdentifier];
267 CFMakeCollectable(infoDict);
277 - (BOOL) isGrowlRunning {
278 return [self isRunning:@"com.Growl.GrowlHelperApp"];
281 - (void) launchGrowl:(BOOL)noMatterWhat {
282 NSString *helperPath = [[GrowlPathUtilities helperAppBundle] bundlePath];
283 NSURL *helperURL = [NSURL fileURLWithPath:helperPath];
285 unsigned options = NSWorkspaceLaunchWithoutAddingToRecents | NSWorkspaceLaunchWithoutActivation | NSWorkspaceLaunchAsync;
287 options |= NSWorkspaceLaunchNewInstance;
288 [[NSWorkspace sharedWorkspace] openURLs:[NSArray arrayWithObject:helperURL]
289 withAppBundleIdentifier:nil
291 additionalEventParamDescriptor:nil
292 launchIdentifiers:NULL];
295 - (void) terminateGrowl {
296 // Ask the Growl Helper App to shutdown via the DNC
297 CFNotificationCenterPostNotification(CFNotificationCenterGetDistributedCenter(),
298 (CFStringRef)GROWL_SHUTDOWN,
301 /*deliverImmediately*/ false);
305 //Simplified accessors
309 - (CFIndex)selectedPosition {
310 return [self integerForKey:GROWL_POSITION_PREFERENCE_KEY];
313 - (BOOL) isBackgroundUpdateCheckEnabled {
314 return [self boolForKey:GrowlUpdateCheckKey];
316 - (void) setIsBackgroundUpdateCheckEnabled:(BOOL)flag {
317 [self setBool:flag forKey:GrowlUpdateCheckKey];
320 - (NSString *) defaultDisplayPluginName {
321 return [self objectForKey:GrowlDisplayPluginKey];
323 - (void) setDefaultDisplayPluginName:(NSString *)name {
324 [self setObject:name forKey:GrowlDisplayPluginKey];
327 - (BOOL) squelchMode {
328 return [self boolForKey:GrowlSquelchModeKey];
330 - (void) setSquelchMode:(BOOL)flag {
331 [self setBool:flag forKey:GrowlSquelchModeKey];
334 - (BOOL) stickyWhenAway {
335 return [self boolForKey:GrowlStickyWhenAwayKey];
337 - (void) setStickyWhenAway:(BOOL)flag {
338 [self setBool:flag forKey:GrowlStickyWhenAwayKey];
341 - (NSNumber*) idleThreshold {
343 return [NSNumber numberWithInteger:[self integerForKey:GrowlStickyIdleThresholdKey]];
345 return [NSNumber numberWithInt:[self integerForKey:GrowlStickyIdleThresholdKey]];
349 - (void) setIdleThreshold:(NSNumber*)value {
350 [self setInteger:[value intValue] forKey:GrowlStickyIdleThresholdKey];
352 #pragma mark Status Item
354 - (BOOL) isGrowlMenuEnabled {
355 return [self boolForKey:GrowlMenuExtraKey];
358 - (void) setGrowlMenuEnabled:(BOOL)state {
359 if (state != [self isGrowlMenuEnabled]) {
360 [self setBool:state forKey:GrowlMenuExtraKey];
362 [self enableGrowlMenu];
364 [self disableGrowlMenu];
370 - (BOOL) loggingEnabled {
371 return [self boolForKey:GrowlLoggingEnabledKey];
374 - (void) setLoggingEnabled:(BOOL)flag {
375 [self setBool:flag forKey:GrowlLoggingEnabledKey];
378 - (BOOL) isGrowlServerEnabled {
379 return [self boolForKey:GrowlStartServerKey];
382 - (void) setGrowlServerEnabled:(BOOL)enabled {
383 [self setBool:enabled forKey:GrowlStartServerKey];
386 #pragma mark Remote Growling
388 - (BOOL) isRemoteRegistrationAllowed {
389 return [self boolForKey:GrowlRemoteRegistrationKey];
392 - (void) setRemoteRegistrationAllowed:(BOOL)flag {
393 [self setBool:flag forKey:GrowlRemoteRegistrationKey];
396 - (NSString *) remotePassword {
397 unsigned char *password;
398 UInt32 passwordLength;
400 status = SecKeychainFindGenericPassword(NULL,
401 (UInt32)strlen(keychainServiceName), keychainServiceName,
402 (UInt32)strlen(keychainAccountName), keychainAccountName,
403 &passwordLength, (void **)&password, NULL);
405 NSString *passwordString;
406 if (status == noErr) {
407 passwordString = (NSString *)CFStringCreateWithBytes(kCFAllocatorDefault, password, passwordLength, kCFStringEncodingUTF8, false);
409 CFMakeCollectable(passwordString);
410 [passwordString autorelease];
411 SecKeychainItemFreeContent(NULL, password);
414 if (status != errSecItemNotFound)
415 NSLog(@"Failed to retrieve password from keychain. Error: %d", status);
416 passwordString = @"";
419 return passwordString;
422 - (void) setRemotePassword:(NSString *)value {
423 const char *password = value ? [value UTF8String] : "";
424 size_t length = strlen(password);
426 SecKeychainItemRef itemRef = nil;
427 status = SecKeychainFindGenericPassword(NULL,
428 (UInt32)strlen(keychainServiceName), keychainServiceName,
429 (UInt32)strlen(keychainAccountName), keychainAccountName,
430 NULL, NULL, &itemRef);
431 if (status == errSecItemNotFound) {
433 status = SecKeychainAddGenericPassword(NULL,
434 (UInt32)strlen(keychainServiceName), keychainServiceName,
435 (UInt32)strlen(keychainAccountName), keychainAccountName,
436 (UInt32)length, password, NULL);
438 NSLog(@"Failed to add password to keychain.");
440 // change existing password
441 SecKeychainAttribute attrs[] = {
442 { kSecAccountItemAttr, (UInt32)strlen(keychainAccountName), (char *)keychainAccountName },
443 { kSecServiceItemAttr, (UInt32)strlen(keychainServiceName), (char *)keychainServiceName }
445 const SecKeychainAttributeList attributes = { (UInt32)sizeof(attrs) / (UInt32)sizeof(attrs[0]), attrs };
446 status = SecKeychainItemModifyAttributesAndData(itemRef, // the item reference
447 &attributes, // no change to attributes
448 (UInt32)length, // length of password
449 password // pointer to password data
454 NSLog(@"Failed to change password in keychain.");
458 - (unsigned short) UDPPort {
459 return [self unsignedShortForKey:GrowlUDPPortKey];
461 - (void) setUDPPort:(unsigned short)value {
462 [self setUnsignedShort:value forKey:GrowlUDPPortKey];
465 - (BOOL) isForwardingEnabled {
466 return [self boolForKey:GrowlEnableForwardKey];
468 - (void) setForwardingEnabled:(BOOL)enabled {
469 [self setBool:enabled forKey:GrowlEnableForwardKey];
474 * @brief Growl preferences changed
476 * Synchronize our NSUserDefaults to immediately get any changes from the disk
478 - (void) growlPreferencesChanged:(NSNotification *)notification {
479 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
481 NSString *object = [notification object];
482 // NSLog(@"%s: %@\n", __func__, object);
483 SYNCHRONIZE_GROWL_PREFS();
484 if (!object || [object isEqualToString:GrowlDisplayPluginKey]) {
485 [self willChangeValueForKey:@"defaultDisplayPluginName"];
486 [self didChangeValueForKey:@"defaultDisplayPluginName"];
488 if (!object || [object isEqualToString:GrowlSquelchModeKey]) {
489 [self willChangeValueForKey:@"squelchMode"];
490 [self didChangeValueForKey:@"squelchMode"];
492 if (!object || [object isEqualToString:GrowlMenuExtraKey]) {
493 [self willChangeValueForKey:@"growlMenuEnabled"];
494 [self didChangeValueForKey:@"growlMenuEnabled"];
496 if (!object || [object isEqualToString:GrowlEnableForwardKey]) {
497 [self willChangeValueForKey:@"forwardingEnabled"];
498 [self didChangeValueForKey:@"forwardingEnabled"];
500 if (!object || [object isEqualToString:GrowlUpdateCheckKey]) {
501 [self willChangeValueForKey:@"backgroundUpdateCheckEnabled"];
502 [self didChangeValueForKey:@"backgroundUpdateCheckEnabled"];
504 if (!object || [object isEqualToString:GrowlStickyWhenAwayKey]) {
505 [self willChangeValueForKey:@"stickyWhenAway"];
506 [self didChangeValueForKey:@"stickyWhenAway"];
508 if (!object || [object isEqualToString:GrowlStickyIdleThresholdKey]) {
509 [self willChangeValueForKey:@"idleThreshold"];
510 [self didChangeValueForKey:@"idleThreshold"];
512 if (!object || [object isEqualToString:GrowlRemoteRegistrationKey]) {
513 [self willChangeValueForKey:@"remoteRegistrationAllowed"];
514 [self didChangeValueForKey:@"remoteRegistrationAllowed"];