/core/externals/google-toolbox-for-mac/AppKit/GTMGoogleSearch.m

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