/core/externals/google-toolbox-for-mac/AppKit/GTMGoogleSearch.m
Objective C | 541 lines | 414 code | 58 blank | 69 comment | 56 complexity | 45343677cbdc2b7bc08819044d616336 MD5 | raw file
1// 2// GTMGoogleSearch.m 3// 4// Copyright 2006-2009 Google Inc. 5// 6// Licensed under the Apache License, Version 2.0 (the "License"); you may not 7// use this file except in compliance with the License. You may obtain a copy 8// of the License at 9// 10// http://www.apache.org/licenses/LICENSE-2.0 11// 12// Unless required by applicable law or agreed to in writing, software 13// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15// License for the specific language governing permissions and limitations under 16// the License. 17// 18 19#import "GTMGoogleSearch.h" 20#import "GTMDefines.h" 21 22#if GTM_IPHONE_SDK 23#import <UIKit/UIKit.h> 24#else 25#import <AppKit/AppKit.h> 26#endif // GTM_IPHONE_SDK 27 28typedef struct { 29 NSString *language; 30 NSString *country; 31 // we don't include a language, we'll use what we get from the OS 32 NSString *defaultDomain; 33} LanguageDefaultInfo; 34 35// 36// this is a seed mapping from languages to domains for google search. 37// this doesn't have to be complete, as it is just a seed. 38// 39// 40static LanguageDefaultInfo kLanguageListDefaultMappingTable[] = { 41 // order is important, first match is taken 42 // if country is |nil|, then only language has to match 43 { @"en", @"US", @"com" }, // english - united states 44 { @"en", @"GB", @"co.uk" }, // english - united kingdom 45 { @"en", @"CA", @"ca" }, // english - canada 46 { @"en", @"AU", @"com.au" }, // english - australia 47 { @"en", @"NZ", @"com" }, // english - new zealand 48 { @"en", @"IE", @"ie" }, // english - ireland 49 { @"en", @"IN", @"co.in" }, // english - india 50 { @"en", @"PH", @"com.ph" }, // english - philippines 51 { @"en", @"SG", @"com.sg" }, // english - singapore 52 { @"en", @"ZA", @"co.za" }, // english - south africa 53 { @"en", @"IL", @"co.il" }, // english - israel 54 { @"en", nil , @"com" }, // english (catch all) 55 { @"fr", @"CA", @"ca" }, // french - canada 56 { @"fr", @"CH", @"ch" }, // french - switzerland 57 { @"fr", nil , @"fr" }, // france 58 { @"it", nil , @"it" }, // italy 59 { @"de", @"AT", @"at" }, // german - austria 60 { @"de", nil , @"de" }, // germany 61 { @"es", @"MX", @"com.mx" }, // spanish - mexico 62 { @"es", @"AR", @"com.ar" }, // spanish - argentina 63 { @"es", @"CL", @"cl" }, // spanish - chile 64 { @"es", @"CO", @"com.co" }, // spanish - colombia 65 { @"es", @"PE", @"com.pe" }, // spanish - peru 66 { @"es", @"VE", @"co.ve" }, // venezuela 67 { @"es", nil , @"es" }, // spain 68 { @"zh", @"TW", @"com.tw" }, // taiwan 69 { @"zh", @"HK", @"com.hk" }, // hong kong 70 { @"zh", nil , @"cn" }, // chinese (catch all) 71 { @"ja", nil , @"co.jp" }, // japan 72 { @"ko", nil , @"co.kr" }, // korea 73 { @"nl", @"BE", @"be" }, // dutch - belgium 74 { @"nl", nil , @"nl" }, // (dutch) netherlands 75 { @"ru", nil , @"ru" }, // russia 76 { @"pt", @"BZ", @"com.br"}, // portuguese - brazil 77 { @"pt", nil , @"pt" }, // portugal 78 { @"sv", nil , @"se" }, // sweden 79 { @"nn", nil , @"no" }, // norway (two variants) 80 { @"nb", nil , @"no" }, // norway (two variants) 81 { @"da", nil , @"dk" }, // denmark 82 { @"fi", nil , @"fi" }, // finland 83 { @"bg", nil , @"bg" }, // bulgaria 84 { @"hr", nil , @"hr" }, // croatia 85 { @"cx", nil , @"cz" }, // czech republic 86 { @"el", nil , @"gr" }, // greece 87 { @"hu", nil , @"co.hu" }, // hungary 88 { @"ro", nil , @"ro" }, // romania 89 { @"sk", nil , @"sk" }, // slovakia 90 { @"sl", nil , @"si" }, // slovenia 91 { @"tr", nil , @"com.tr" }, // turkey 92 { @"my", nil , @"com.my" }, // malaysia 93 { @"th", nil , @"co.th" }, // thailand 94 { @"uk", nil , @"com.ua" }, // ukraine 95 { @"vi", nil , @"com.vn" }, // vietnam 96 { @"af", nil , @"com.za" }, // south africa (afrikaans) 97 { @"hi", nil , @"co.in" }, // india (hindi) 98 { @"id", nil , @"co.id" }, // indonesia 99 { @"pl", nil , @"pl" }, // poland 100}; 101 102// the notification we use for syncing up instances in different processes 103static NSString *const kNotificationName 104 = @"com.google.GoogleSearchAllApps.prefsWritten"; 105 106// this is the bundle id we use for the pref file used for all apps 107static CFStringRef const kAllAppsBuildIdentifier 108 = CFSTR("com.google.GoogleSearchAllApps"); 109 110static CFStringRef const kPreferredDomainPrefKey 111 = CFSTR("com.google.PreferredDomain"); 112static CFStringRef const kPreferredLanguagePrefKey 113 = CFSTR("com.google.PreferredLanguage"); 114 115static NSString *const kDefaultDomain = @"com"; 116static NSString *const kDefaultLanguage = @"en"; 117 118#define SEARCH_URL_TEMPLATE @"http://www.google.%@/%@?%@" 119 120@interface GTMGoogleSearch (PrivateMethods) 121- (void)defaultDomain:(NSString**)preferedDomain 122 language:(NSString**)preferredLanguage; 123- (void)reloadAllAppCachedValues:(NSNotification*)notification; 124- (void)updateAllAppsDomain:(NSString*)domain language:(NSString*)language; 125@end 126 127 128@implementation GTMGoogleSearch 129 130+ (GTMGoogleSearch *)sharedInstance { 131 static GTMGoogleSearch *obj; 132 if (!obj) { 133 obj = [[self alloc] init]; 134 } 135 return obj; 136} 137 138- (id)init { 139 self = [super init]; 140 if (self != nil) { 141#if GTM_GOOGLE_SEARCH_SUPPORTS_DISTRIBUTED_NOTIFICATIONS 142 // register for the notification 143 NSDistributedNotificationCenter *distCenter = 144 [NSDistributedNotificationCenter defaultCenter]; 145 [distCenter addObserver:self 146 selector:@selector(reloadAllAppCachedValues:) 147 name:kNotificationName 148 object:nil]; 149#endif // GTM_GOOGLE_SEARCH_SUPPORTS_DISTRIBUTED_NOTIFICATIONS 150 // load the allApps value 151 [self reloadAllAppCachedValues:nil]; 152 153 // load the cur app value 154 CFStringRef domain 155 = CFPreferencesCopyValue(kPreferredDomainPrefKey, 156 kCFPreferencesCurrentApplication, 157 kCFPreferencesCurrentUser, 158 kCFPreferencesAnyHost); 159 CFStringRef lang = CFPreferencesCopyValue(kPreferredLanguagePrefKey, 160 kCFPreferencesCurrentApplication, 161 kCFPreferencesCurrentUser, 162 kCFPreferencesAnyHost); 163 164 // make sure we got values for both and domain is not empty 165 if (domain && CFStringGetLength(domain) == 0) { 166 CFRelease(domain); 167 domain = nil; 168 if (lang) { 169 CFRelease(lang); 170 lang = nil; 171 } 172 } 173 174 curAppCachedDomain_ = (NSString *)domain; 175 curAppCachedLanguage_ = (NSString *)lang; 176 177 NSBundle *bundle = [NSBundle mainBundle]; 178 179 NSDictionary *appArgs 180 = [bundle objectForInfoDictionaryKey:GTMGoogleSearchClientAppArgsKey]; 181 globalSearchArguments_ = [appArgs retain]; 182 } 183 return self; 184} 185 186#if GTM_GOOGLE_SEARCH_SUPPORTS_DISTRIBUTED_NOTIFICATIONS 187- (void)finalize { 188 [[NSDistributedNotificationCenter defaultCenter] removeObserver:self]; 189 [super finalize]; 190} 191#endif // GTM_GOOGLE_SEARCH_SUPPORTS_DISTRIBUTED_NOTIFICATIONS 192 193- (void)dealloc { 194#if GTM_GOOGLE_SEARCH_SUPPORTS_DISTRIBUTED_NOTIFICATIONS 195 [[NSDistributedNotificationCenter defaultCenter] removeObserver:self]; 196#endif // GTM_GOOGLE_SEARCH_SUPPORTS_DISTRIBUTED_NOTIFICATIONS 197 [allAppsCachedDomain_ release]; 198 [allAppsCachedLanguage_ release]; 199 [curAppCachedDomain_ release]; 200 [curAppCachedLanguage_ release]; 201 [globalSearchArguments_ release]; 202 [super dealloc]; 203} 204 205- (void)preferredDomain:(NSString **)domain 206 language:(NSString**)language 207 areCurrentAppOnly:(BOOL*)currentAppOnly { 208 BOOL localCurrentAppOnly = YES; 209 NSString *localDomain = curAppCachedDomain_; 210 NSString *localLanguage = curAppCachedLanguage_; 211 212 // if either one wasn't there, drop both, and use any app if we can 213 if (!localDomain || !localLanguage) { 214 localCurrentAppOnly = NO; 215 localDomain = allAppsCachedDomain_; 216 localLanguage = allAppsCachedLanguage_; 217 218 // if we didn't get anything from the prefs, go with the defaults 219 if (!localDomain || !localLanguage) { 220 // if either one wasn't there, drop both, and use defaults 221 [self defaultDomain:&localDomain language:&localLanguage]; 222 } 223 } 224 if (!localDomain || !localLanguage) { 225 _GTMDevLog(@"GTMGoogleSearch: Failed to get the preferred domain/language " 226 @"from prefs or defaults"); 227 } 228 if (language) { 229 *language = [[localLanguage retain] autorelease]; 230 } 231 if (domain) { 232 *domain = [[localDomain retain] autorelease]; 233 } 234 if (currentAppOnly) { 235 *currentAppOnly = localCurrentAppOnly; 236 } 237} 238 239- (void)updatePreferredDomain:(NSString*)domain 240 language:(NSString*)language 241 currentApplicationOnly:(BOOL)currentAppOnly { 242 // valid inputs? 243 if (!domain || ![domain length] || !language) { 244 return; 245 } 246 247 if (currentAppOnly) { 248 // if they are the same, don't do anything 249 if ((domain == nil && curAppCachedDomain_ == nil && 250 language == nil && curAppCachedLanguage_ == nil) || 251 ([domain isEqualToString:curAppCachedDomain_] && 252 [language isEqualToString:curAppCachedLanguage_])) { 253 return; 254 } 255 256 // save them out 257 CFPreferencesSetValue(kPreferredDomainPrefKey, 258 (CFStringRef)domain, 259 kCFPreferencesCurrentApplication, 260 kCFPreferencesCurrentUser, 261 kCFPreferencesAnyHost); 262 CFPreferencesSetValue(kPreferredLanguagePrefKey, 263 (CFStringRef)language, 264 kCFPreferencesCurrentApplication, 265 kCFPreferencesCurrentUser, 266 kCFPreferencesAnyHost); 267 CFPreferencesSynchronize(kCFPreferencesCurrentApplication, 268 kCFPreferencesCurrentUser, 269 kCFPreferencesAnyHost); 270 // update our locals 271 [curAppCachedDomain_ release]; 272 [curAppCachedLanguage_ release]; 273 curAppCachedDomain_ = [domain copy]; 274 curAppCachedLanguage_ = [language copy]; 275 } else { 276 // Set the "any application" values 277 [self updateAllAppsDomain:domain language:language]; 278 279 // Clear the current application values (if there were any) 280 [self clearPreferredDomainAndLanguageForCurrentApplication]; 281 } 282} 283 284- (void)clearPreferredDomainAndLanguageForCurrentApplication { 285 // flush what's in the file 286 CFPreferencesSetValue(kPreferredDomainPrefKey, 287 NULL, 288 kCFPreferencesCurrentApplication, 289 kCFPreferencesCurrentUser, 290 kCFPreferencesAnyHost); 291 CFPreferencesSetValue(kPreferredLanguagePrefKey, 292 NULL, 293 kCFPreferencesCurrentApplication, 294 kCFPreferencesCurrentUser, 295 kCFPreferencesAnyHost); 296 CFPreferencesSynchronize(kCFPreferencesCurrentApplication, 297 kCFPreferencesCurrentUser, 298 kCFPreferencesAnyHost); 299 // clear our locals 300 [curAppCachedDomain_ release]; 301 [curAppCachedLanguage_ release]; 302 curAppCachedDomain_ = nil; 303 curAppCachedLanguage_ = nil; 304} 305 306- (void)clearPreferredDomainAndLanguageForAllApps { 307 // nil/nil to clear things out, this will also update our cached values. 308 [self updateAllAppsDomain:nil language:nil]; 309} 310 311- (NSDictionary *)globalSearchArguments { 312 return globalSearchArguments_; 313} 314 315- (void)setGlobalSearchArguments:(NSDictionary *)args { 316 [globalSearchArguments_ autorelease]; 317 globalSearchArguments_ = [args copy]; 318} 319 320- (NSString*)searchURLFor:(NSString*)queryText 321 ofType:(NSString*)type 322 arguments:(NSDictionary *)localArgs { 323 if (!type) { 324 return nil; 325 } 326 327 NSString *language; 328 NSString *domain; 329 [self preferredDomain:&domain 330 language:&language 331 areCurrentAppOnly:NULL]; 332 333 NSMutableDictionary *args 334 = [NSMutableDictionary dictionaryWithObjectsAndKeys: 335 @"UTF-8", @"ie", 336 @"UTF-8", @"oe", 337 language, @"hl", 338 nil]; 339 if (queryText) { 340 [args setObject:queryText forKey:@"q"]; 341 } 342 343 NSDictionary *globalSearchArgs = [self globalSearchArguments]; 344 if (globalSearchArgs) { 345 [args addEntriesFromDictionary:globalSearchArgs]; 346 } 347 if (localArgs) { 348 [args addEntriesFromDictionary:localArgs]; 349 } 350 351 NSMutableArray *clientArgs = [NSMutableArray array]; 352 NSString *key; 353 NSNull *nsNull = [NSNull null]; 354 GTM_FOREACH_KEY(key, args) { 355 NSString *object = [args objectForKey:key]; 356 if (![object isEqual:nsNull]) { 357#if DEBUG 358 // In debug we check key and object for things that should be escaped. 359 // Note that percent is not in there because escaped strings will have 360 // percents in them 361 NSCharacterSet *cs = [NSCharacterSet characterSetWithCharactersInString: 362 @"!*'();:@&=+$,/?#[] "]; 363 NSRange range = [key rangeOfCharacterFromSet:cs]; 364 if (range.location != NSNotFound) { 365 _GTMDevLog(@"Unescaped string %@ in argument pair {%@, %@} in -[%@ %@]", 366 key, key, object, [self class], NSStringFromSelector(_cmd)); 367 } 368 range = [object rangeOfCharacterFromSet:cs]; 369 if (range.location != NSNotFound) { 370 _GTMDevLog(@"Unescaped string %@ in argument pair {%@,%@ } in -[%@ %@]", 371 object, key, object, [self class], 372 NSStringFromSelector(_cmd)); 373 } 374#endif // DEBUG 375 NSString *arg = [NSString stringWithFormat:@"%@=%@", key, object]; 376 [clientArgs addObject:arg]; 377 } 378 } 379 NSString *clientArg = [clientArgs componentsJoinedByString:@"&"]; 380 NSString *url = [NSString stringWithFormat:SEARCH_URL_TEMPLATE, 381 domain, type, clientArg]; 382 return url; 383} 384 385- (BOOL)performQuery:(NSString*)queryText 386 ofType:(NSString *)type 387 arguments:(NSDictionary *)localArgs { 388 BOOL success = NO; 389 NSString *urlString = [self searchURLFor:queryText 390 ofType:type 391 arguments:localArgs]; 392 if (urlString) { 393 NSURL *url = [NSURL URLWithString:urlString]; 394 if (url) { 395#if GTM_IPHONE_SDK 396 success = [[UIApplication sharedApplication] openURL:url]; 397#else // GTM_IPHONE_SDK 398 success = [[NSWorkspace sharedWorkspace] openURL:url]; 399#endif // GTM_IPHONE_SDK 400 } 401 } 402 return success; 403} 404 405@end 406 407 408@implementation GTMGoogleSearch (PrivateMethods) 409 410- (void)defaultDomain:(NSString**)preferredDomain 411 language:(NSString**)preferredLanguage { 412 // must have both 413 if (!preferredDomain || !preferredLanguage) { 414 return; 415 } 416 417 // make sure they are clear to start 418 *preferredDomain = nil; 419 *preferredLanguage = nil; 420 421 // loop over their language list trying to find something we have in 422 // out default table. 423 424 NSUserDefaults* defs = [NSUserDefaults standardUserDefaults]; 425 NSArray* languages = [defs objectForKey:@"AppleLanguages"]; 426 // the current locale is only based on what languages the running apps is 427 // localized to, so we stick that at the end in case we weren't able to 428 // find anything else as a match, we'll match that. 429 languages = 430 [languages arrayByAddingObject:[[NSLocale currentLocale] localeIdentifier]]; 431 432 NSEnumerator *enumerator = [languages objectEnumerator]; 433 NSString *localeIdentifier; 434 while ((localeIdentifier = [enumerator nextObject])) { 435 NSDictionary *localeParts 436 = [NSLocale componentsFromLocaleIdentifier:localeIdentifier]; 437 NSString *localeLanguage = [localeParts objectForKey:NSLocaleLanguageCode]; 438 // we don't use NSLocaleScriptCode for now 439 NSString *localeCountry = [localeParts objectForKey:NSLocaleCountryCode]; 440 441 LanguageDefaultInfo *scan = kLanguageListDefaultMappingTable; 442 LanguageDefaultInfo *end = (scan + (sizeof(kLanguageListDefaultMappingTable) 443 / sizeof(LanguageDefaultInfo))); 444 // find a match 445 // check language, and if country is not nil, check that 446 for ( ; scan < end ; ++scan) { 447 if ([localeLanguage isEqualToString:scan->language] && 448 (!(scan->country) || [localeCountry isEqualToString:scan->country])) { 449 *preferredDomain = scan->defaultDomain; 450 *preferredLanguage = localeLanguage; 451 return; // out of here 452 } 453 } 454 } 455 456 *preferredDomain = kDefaultDomain; 457 *preferredLanguage = kDefaultLanguage; 458} 459 460// -reloadAllAppCachedValues: 461// 462- (void)reloadAllAppCachedValues:(NSNotification*)notification { 463 // drop the old... 464 [allAppsCachedDomain_ release]; 465 [allAppsCachedLanguage_ release]; 466 allAppsCachedDomain_ = nil; 467 allAppsCachedLanguage_ = nil; 468 469 // load the new 470 CFPreferencesSynchronize(kAllAppsBuildIdentifier, 471 kCFPreferencesCurrentUser, 472 kCFPreferencesAnyHost); 473 CFStringRef domain = CFPreferencesCopyValue(kPreferredDomainPrefKey, 474 kAllAppsBuildIdentifier, 475 kCFPreferencesCurrentUser, 476 kCFPreferencesAnyHost); 477 CFStringRef lang = CFPreferencesCopyValue(kPreferredLanguagePrefKey, 478 kAllAppsBuildIdentifier, 479 kCFPreferencesCurrentUser, 480 kCFPreferencesAnyHost); 481 482 // make sure we got values for both and domain is not empty 483 if (domain && CFStringGetLength(domain) == 0) { 484 CFRelease(domain); 485 domain = nil; 486 if (lang) { 487 CFRelease(lang); 488 lang = nil; 489 } 490 } 491 492 allAppsCachedDomain_ = (NSString *)domain; 493 allAppsCachedLanguage_ = (NSString *)lang; 494} 495 496// -updateAllAppsDomain:language: 497// 498- (void)updateAllAppsDomain:(NSString*)domain language:(NSString*)language { 499 // domain and language can be nil to clear the values 500 501 // if they are the same, don't do anything 502 if ((domain == nil && allAppsCachedDomain_ == nil && 503 language == nil && allAppsCachedLanguage_ == nil) || 504 ([domain isEqualToString:allAppsCachedDomain_] && 505 [language isEqualToString:allAppsCachedLanguage_])) { 506 return; 507 } 508 509 // write it to the file 510 CFPreferencesSetValue(kPreferredDomainPrefKey, 511 (CFStringRef)domain, 512 kAllAppsBuildIdentifier, 513 kCFPreferencesCurrentUser, 514 kCFPreferencesAnyHost); 515 CFPreferencesSetValue(kPreferredLanguagePrefKey, 516 (CFStringRef)language, 517 kAllAppsBuildIdentifier, 518 kCFPreferencesCurrentUser, 519 kCFPreferencesAnyHost); 520 CFPreferencesSynchronize(kAllAppsBuildIdentifier, 521 kCFPreferencesCurrentUser, 522 kCFPreferencesAnyHost); 523 524 // update our values 525 [allAppsCachedDomain_ release]; 526 [allAppsCachedLanguage_ release]; 527 allAppsCachedDomain_ = [domain copy]; 528 allAppsCachedLanguage_ = [language copy]; 529 530#if GTM_GOOGLE_SEARCH_SUPPORTS_DISTRIBUTED_NOTIFICATIONS 531 // NOTE: we'll go ahead and reload when this comes back to ourselves since 532 // there is a race here if two folks wrote at about the same time. 533 NSDistributedNotificationCenter *distCenter = 534 [NSDistributedNotificationCenter defaultCenter]; 535 [distCenter postNotificationName:kNotificationName 536 object:nil 537 userInfo:nil]; 538#endif // GTM_GOOGLE_SEARCH_SUPPORTS_DISTRIBUTED_NOTIFICATIONS 539} 540 541@end