1 // Copyright 2013 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include "chrome/browser/search/hotword_service.h" 6 7 #include "base/i18n/case_conversion.h" 8 #include "base/metrics/field_trial.h" 9 #include "base/metrics/histogram.h" 10 #include "base/path_service.h" 11 #include "base/prefs/pref_service.h" 12 #include "chrome/browser/browser_process.h" 13 #include "chrome/browser/chrome_notification_types.h" 14 #include "chrome/browser/extensions/extension_service.h" 15 #include "chrome/browser/extensions/pending_extension_manager.h" 16 #include "chrome/browser/extensions/updater/extension_updater.h" 17 #include "chrome/browser/extensions/webstore_startup_installer.h" 18 #include "chrome/browser/plugins/plugin_prefs.h" 19 #include "chrome/browser/profiles/profile.h" 20 #include "chrome/browser/search/hotword_service_factory.h" 21 #include "chrome/common/chrome_paths.h" 22 #include "chrome/common/extensions/extension_constants.h" 23 #include "chrome/common/pref_names.h" 24 #include "content/public/browser/browser_thread.h" 25 #include "content/public/browser/notification_service.h" 26 #include "content/public/browser/plugin_service.h" 27 #include "content/public/common/webplugininfo.h" 28 #include "extensions/browser/extension_system.h" 29 #include "extensions/common/extension.h" 30 #include "extensions/common/one_shot_event.h" 31 #include "grit/generated_resources.h" 32 #include "ui/base/l10n/l10n_util.h" 33 34 // The whole file relies on the extension systems but this file is built on 35 // some non-extension supported platforms and including an API header will cause 36 // a compile error since it depends on header files generated by .idl. 37 // TODO(mukai): clean up file dependencies and remove this clause. 38 #if defined(ENABLE_EXTENSIONS) 39 #include "chrome/browser/extensions/api/hotword_private/hotword_private_api.h" 40 #endif 41 42 #if defined(ENABLE_EXTENSIONS) 43 using extensions::BrowserContextKeyedAPIFactory; 44 using extensions::HotwordPrivateEventService; 45 #endif 46 47 namespace { 48 49 // Allowed languages for hotwording. 50 static const char* kSupportedLocales[] = { 51 "en", 52 "de", 53 "fr", 54 "ru" 55 }; 56 57 // Enum describing the state of the hotword preference. 58 // This is used for UMA stats -- do not reorder or delete items; only add to 59 // the end. 60 enum HotwordEnabled { 61 UNSET = 0, // The hotword preference has not been set. 62 ENABLED, // The hotword preference is enabled. 63 DISABLED, // The hotword preference is disabled. 64 NUM_HOTWORD_ENABLED_METRICS 65 }; 66 67 // Enum describing the availability state of the hotword extension. 68 // This is used for UMA stats -- do not reorder or delete items; only add to 69 // the end. 70 enum HotwordExtensionAvailability { 71 UNAVAILABLE = 0, 72 AVAILABLE, 73 PENDING_DOWNLOAD, 74 DISABLED_EXTENSION, 75 NUM_HOTWORD_EXTENSION_AVAILABILITY_METRICS 76 }; 77 78 // Enum describing the types of errors that can arise when determining 79 // if hotwording can be used. NO_ERROR is used so it can be seen how often 80 // errors arise relative to when they do not. 81 // This is used for UMA stats -- do not reorder or delete items; only add to 82 // the end. 83 enum HotwordError { 84 NO_HOTWORD_ERROR = 0, 85 GENERIC_HOTWORD_ERROR, 86 NACL_HOTWORD_ERROR, 87 MICROPHONE_HOTWORD_ERROR, 88 NUM_HOTWORD_ERROR_METRICS 89 }; 90 91 void RecordExtensionAvailabilityMetrics( 92 ExtensionService* service, 93 const extensions::Extension* extension) { 94 HotwordExtensionAvailability availability_state = UNAVAILABLE; 95 if (extension) { 96 availability_state = AVAILABLE; 97 } else if (service->pending_extension_manager() && 98 service->pending_extension_manager()->IsIdPending( 99 extension_misc::kHotwordExtensionId)) { 100 availability_state = PENDING_DOWNLOAD; 101 } else if (!service->IsExtensionEnabled( 102 extension_misc::kHotwordExtensionId)) { 103 availability_state = DISABLED_EXTENSION; 104 } 105 UMA_HISTOGRAM_ENUMERATION("Hotword.HotwordExtensionAvailability", 106 availability_state, 107 NUM_HOTWORD_EXTENSION_AVAILABILITY_METRICS); 108 } 109 110 void RecordLoggingMetrics(Profile* profile) { 111 // If the user is not opted in to hotword voice search, the audio logging 112 // metric is not valid so it is not recorded. 113 if (!profile->GetPrefs()->GetBoolean(prefs::kHotwordSearchEnabled)) 114 return; 115 116 UMA_HISTOGRAM_BOOLEAN( 117 "Hotword.HotwordAudioLogging", 118 profile->GetPrefs()->GetBoolean(prefs::kHotwordAudioLoggingEnabled)); 119 } 120 121 void RecordErrorMetrics(int error_message) { 122 HotwordError error = NO_HOTWORD_ERROR; 123 switch (error_message) { 124 case IDS_HOTWORD_GENERIC_ERROR_MESSAGE: 125 error = GENERIC_HOTWORD_ERROR; 126 break; 127 case IDS_HOTWORD_NACL_DISABLED_ERROR_MESSAGE: 128 error = NACL_HOTWORD_ERROR; 129 break; 130 case IDS_HOTWORD_MICROPHONE_ERROR_MESSAGE: 131 error = MICROPHONE_HOTWORD_ERROR; 132 break; 133 default: 134 error = NO_HOTWORD_ERROR; 135 } 136 137 UMA_HISTOGRAM_ENUMERATION("Hotword.HotwordError", 138 error, 139 NUM_HOTWORD_ERROR_METRICS); 140 } 141 142 ExtensionService* GetExtensionService(Profile* profile) { 143 CHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); 144 145 extensions::ExtensionSystem* extension_system = 146 extensions::ExtensionSystem::Get(profile); 147 if (extension_system) 148 return extension_system->extension_service(); 149 return NULL; 150 } 151 152 std::string GetCurrentLocale(Profile* profile) { 153 std::string locale = 154 #if defined(OS_CHROMEOS) 155 // On ChromeOS locale is per-profile. 156 profile->GetPrefs()->GetString(prefs::kApplicationLocale); 157 #else 158 g_browser_process->GetApplicationLocale(); 159 #endif 160 return locale; 161 } 162 163 } // namespace 164 165 namespace hotword_internal { 166 // Constants for the hotword field trial. 167 const char kHotwordFieldTrialName[] = "VoiceTrigger"; 168 const char kHotwordFieldTrialDisabledGroupName[] = "Disabled"; 169 // Old preference constant. 170 const char kHotwordUnusablePrefName[] = "hotword.search_enabled"; 171 } // namespace hotword_internal 172 173 // static 174 bool HotwordService::DoesHotwordSupportLanguage(Profile* profile) { 175 std::string normalized_locale = 176 l10n_util::NormalizeLocale(GetCurrentLocale(profile)); 177 StringToLowerASCII(&normalized_locale); 178 179 for (size_t i = 0; i < arraysize(kSupportedLocales); i++) { 180 if (normalized_locale.compare(0, 2, kSupportedLocales[i]) == 0) 181 return true; 182 } 183 return false; 184 } 185 186 HotwordService::HotwordService(Profile* profile) 187 : profile_(profile), 188 extension_registry_observer_(this), 189 client_(NULL), 190 error_message_(0), 191 reinstall_pending_(false), 192 weak_factory_(this) { 193 extension_registry_observer_.Add(extensions::ExtensionRegistry::Get(profile)); 194 // This will be called during profile initialization which is a good time 195 // to check the user's hotword state. 196 HotwordEnabled enabled_state = UNSET; 197 if (profile_->GetPrefs()->HasPrefPath(prefs::kHotwordSearchEnabled)) { 198 if (profile_->GetPrefs()->GetBoolean(prefs::kHotwordSearchEnabled)) 199 enabled_state = ENABLED; 200 else 201 enabled_state = DISABLED; 202 } else { 203 // If the preference has not been set the hotword extension should 204 // not be running. However, this should only be done if auto-install 205 // is enabled which is gated through the IsHotwordAllowed check. 206 if (IsHotwordAllowed()) 207 DisableHotwordExtension(GetExtensionService(profile_)); 208 } 209 UMA_HISTOGRAM_ENUMERATION("Hotword.Enabled", enabled_state, 210 NUM_HOTWORD_ENABLED_METRICS); 211 212 pref_registrar_.Init(profile_->GetPrefs()); 213 pref_registrar_.Add( 214 prefs::kHotwordSearchEnabled, 215 base::Bind(&HotwordService::OnHotwordSearchEnabledChanged, 216 base::Unretained(this))); 217 218 registrar_.Add(this, 219 chrome::NOTIFICATION_BROWSER_WINDOW_READY, 220 content::NotificationService::AllSources()); 221 222 extensions::ExtensionSystem::Get(profile_)->ready().Post( 223 FROM_HERE, 224 base::Bind(base::IgnoreResult( 225 &HotwordService::MaybeReinstallHotwordExtension), 226 weak_factory_.GetWeakPtr())); 227 228 // Clear the old user pref because it became unusable. 229 // TODO(rlp): Remove this code per crbug.com/358789. 230 if (profile_->GetPrefs()->HasPrefPath( 231 hotword_internal::kHotwordUnusablePrefName)) { 232 profile_->GetPrefs()->ClearPref(hotword_internal::kHotwordUnusablePrefName); 233 } 234 } 235 236 HotwordService::~HotwordService() { 237 } 238 239 void HotwordService::Observe(int type, 240 const content::NotificationSource& source, 241 const content::NotificationDetails& details) { 242 if (type == chrome::NOTIFICATION_BROWSER_WINDOW_READY) { 243 // The microphone monitor must be initialized as the page is loading 244 // so that the state of the microphone is available when the page 245 // loads. The Ok Google Hotword setting will display an error if there 246 // is no microphone but this information will not be up-to-date unless 247 // the monitor had already been started. Furthermore, the pop up to 248 // opt in to hotwording won't be available if it thinks there is no 249 // microphone. There is no hard guarantee that the monitor will actually 250 // be up by the time it's needed, but this is the best we can do without 251 // starting it at start up which slows down start up too much. 252 // The content/media for microphone uses the same observer design and 253 // makes use of the same audio device monitor. 254 HotwordServiceFactory::GetInstance()->UpdateMicrophoneState(); 255 } 256 } 257 258 void HotwordService::OnExtensionUninstalled( 259 content::BrowserContext* browser_context, 260 const extensions::Extension* extension) { 261 CHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); 262 263 if (extension->id() != extension_misc::kHotwordExtensionId || 264 profile_ != Profile::FromBrowserContext(browser_context) || 265 !GetExtensionService(profile_)) 266 return; 267 268 // If the extension wasn't uninstalled due to language change, don't try to 269 // reinstall it. 270 if (!reinstall_pending_) 271 return; 272 273 InstallHotwordExtensionFromWebstore(); 274 SetPreviousLanguagePref(); 275 } 276 277 void HotwordService::InstallHotwordExtensionFromWebstore() { 278 #if defined(ENABLE_EXTENSIONS) 279 installer_ = new extensions::WebstoreStartupInstaller( 280 extension_misc::kHotwordExtensionId, 281 profile_, 282 false, 283 extensions::WebstoreStandaloneInstaller::Callback()); 284 installer_->BeginInstall(); 285 #endif 286 } 287 288 void HotwordService::OnExtensionInstalled( 289 content::BrowserContext* browser_context, 290 const extensions::Extension* extension) { 291 292 if (extension->id() != extension_misc::kHotwordExtensionId || 293 profile_ != Profile::FromBrowserContext(browser_context)) 294 return; 295 296 // If the previous locale pref has never been set, set it now since 297 // the extension has been installed. 298 if (!profile_->GetPrefs()->HasPrefPath(prefs::kHotwordPreviousLanguage)) 299 SetPreviousLanguagePref(); 300 301 // If MaybeReinstallHotwordExtension already triggered an uninstall, we 302 // don't want to loop and trigger another uninstall-install cycle. 303 // However, if we arrived here via an uninstall-triggered-install (and in 304 // that case |reinstall_pending_| will be true) then we know install 305 // has completed and we can reset |reinstall_pending_|. 306 if (!reinstall_pending_) 307 MaybeReinstallHotwordExtension(); 308 else 309 reinstall_pending_ = false; 310 311 // Now that the extension is installed, if the user has not selected 312 // the preference on, make sure it is turned off. 313 // 314 // Disabling the extension automatically on install should only occur 315 // if the user is in the field trial for auto-install which is gated 316 // by the IsHotwordAllowed check. The check for IsHotwordAllowed() here 317 // can be removed once it's known that few people have manually 318 // installed extension. 319 if (IsHotwordAllowed() && 320 !profile_->GetPrefs()->GetBoolean(prefs::kHotwordSearchEnabled)) { 321 DisableHotwordExtension(GetExtensionService(profile_)); 322 } 323 } 324 325 bool HotwordService::MaybeReinstallHotwordExtension() { 326 CHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); 327 328 ExtensionService* extension_service = GetExtensionService(profile_); 329 if (!extension_service) 330 return false; 331 332 const extensions::Extension* extension = extension_service->GetExtensionById( 333 extension_misc::kHotwordExtensionId, true); 334 if (!extension) 335 return false; 336 337 // If the extension is currently pending, return and we'll check again 338 // after the install is finished. 339 extensions::PendingExtensionManager* pending_manager = 340 extension_service->pending_extension_manager(); 341 if (pending_manager->IsIdPending(extension->id())) 342 return false; 343 344 // If there is already a pending request from HotwordService, don't try 345 // to uninstall either. 346 if (reinstall_pending_) 347 return false; 348 349 // Check if the current locale matches the previous. If they don't match, 350 // uninstall the extension. 351 if (!ShouldReinstallHotwordExtension()) 352 return false; 353 354 // Ensure the call to OnExtensionUninstalled was triggered by a language 355 // change so it's okay to reinstall. 356 reinstall_pending_ = true; 357 358 return UninstallHotwordExtension(extension_service); 359 } 360 361 bool HotwordService::UninstallHotwordExtension( 362 ExtensionService* extension_service) { 363 base::string16 error; 364 if (!extension_service->UninstallExtension( 365 extension_misc::kHotwordExtensionId, true, &error)) { 366 LOG(WARNING) << "Cannot uninstall extension with id " 367 << extension_misc::kHotwordExtensionId 368 << ": " << error; 369 reinstall_pending_ = false; 370 return false; 371 } 372 return true; 373 } 374 375 bool HotwordService::IsServiceAvailable() { 376 error_message_ = 0; 377 378 // Determine if the extension is available. 379 extensions::ExtensionSystem* system = 380 extensions::ExtensionSystem::Get(profile_); 381 ExtensionService* service = system->extension_service(); 382 // Include disabled extensions (true parameter) since it may not be enabled 383 // if the user opted out. 384 const extensions::Extension* extension = 385 service->GetExtensionById(extension_misc::kHotwordExtensionId, true); 386 if (!extension) 387 error_message_ = IDS_HOTWORD_GENERIC_ERROR_MESSAGE; 388 389 RecordExtensionAvailabilityMetrics(service, extension); 390 RecordLoggingMetrics(profile_); 391 392 // NaCl and its associated functions are not available on most mobile 393 // platforms. ENABLE_EXTENSIONS covers those platforms and hey would not 394 // allow Hotwording anyways since it is an extension. 395 #if defined(ENABLE_EXTENSIONS) 396 // Determine if NaCl is available. 397 bool nacl_enabled = false; 398 base::FilePath path; 399 if (PathService::Get(chrome::FILE_NACL_PLUGIN, &path)) { 400 content::WebPluginInfo info; 401 PluginPrefs* plugin_prefs = PluginPrefs::GetForProfile(profile_).get(); 402 if (content::PluginService::GetInstance()->GetPluginInfoByPath(path, &info)) 403 nacl_enabled = plugin_prefs->IsPluginEnabled(info); 404 } 405 if (!nacl_enabled) 406 error_message_ = IDS_HOTWORD_NACL_DISABLED_ERROR_MESSAGE; 407 #endif 408 409 RecordErrorMetrics(error_message_); 410 411 // Determine if the proper audio capabilities exist. 412 bool audio_capture_allowed = 413 profile_->GetPrefs()->GetBoolean(prefs::kAudioCaptureAllowed); 414 if (!audio_capture_allowed || !HotwordServiceFactory::IsMicrophoneAvailable()) 415 error_message_ = IDS_HOTWORD_MICROPHONE_ERROR_MESSAGE; 416 417 return (error_message_ == 0) && IsHotwordAllowed(); 418 } 419 420 bool HotwordService::IsHotwordAllowed() { 421 std::string group = base::FieldTrialList::FindFullName( 422 hotword_internal::kHotwordFieldTrialName); 423 return !group.empty() && 424 group != hotword_internal::kHotwordFieldTrialDisabledGroupName && 425 DoesHotwordSupportLanguage(profile_); 426 } 427 428 bool HotwordService::IsOptedIntoAudioLogging() { 429 // Do not opt the user in if the preference has not been set. 430 return 431 profile_->GetPrefs()->HasPrefPath(prefs::kHotwordAudioLoggingEnabled) && 432 profile_->GetPrefs()->GetBoolean(prefs::kHotwordAudioLoggingEnabled); 433 } 434 435 void HotwordService::EnableHotwordExtension( 436 ExtensionService* extension_service) { 437 if (extension_service) 438 extension_service->EnableExtension(extension_misc::kHotwordExtensionId); 439 } 440 441 void HotwordService::DisableHotwordExtension( 442 ExtensionService* extension_service) { 443 if (extension_service) { 444 extension_service->DisableExtension( 445 extension_misc::kHotwordExtensionId, 446 extensions::Extension::DISABLE_USER_ACTION); 447 } 448 } 449 450 void HotwordService::OnHotwordSearchEnabledChanged( 451 const std::string& pref_name) { 452 DCHECK_EQ(pref_name, std::string(prefs::kHotwordSearchEnabled)); 453 454 ExtensionService* extension_service = GetExtensionService(profile_); 455 if (profile_->GetPrefs()->GetBoolean(prefs::kHotwordSearchEnabled)) 456 EnableHotwordExtension(extension_service); 457 else 458 DisableHotwordExtension(extension_service); 459 } 460 461 void HotwordService::RequestHotwordSession(HotwordClient* client) { 462 #if defined(ENABLE_EXTENSIONS) 463 if (!IsServiceAvailable() || client_) 464 return; 465 466 client_ = client; 467 468 HotwordPrivateEventService* event_service = 469 BrowserContextKeyedAPIFactory<HotwordPrivateEventService>::Get(profile_); 470 if (event_service) 471 event_service->OnHotwordSessionRequested(); 472 #endif 473 } 474 475 void HotwordService::StopHotwordSession(HotwordClient* client) { 476 #if defined(ENABLE_EXTENSIONS) 477 if (!IsServiceAvailable()) 478 return; 479 480 DCHECK(client_ == client); 481 482 client_ = NULL; 483 HotwordPrivateEventService* event_service = 484 BrowserContextKeyedAPIFactory<HotwordPrivateEventService>::Get(profile_); 485 if (event_service) 486 event_service->OnHotwordSessionStopped(); 487 #endif 488 } 489 490 void HotwordService::SetPreviousLanguagePref() { 491 profile_->GetPrefs()->SetString(prefs::kHotwordPreviousLanguage, 492 GetCurrentLocale(profile_)); 493 } 494 495 bool HotwordService::ShouldReinstallHotwordExtension() { 496 // If there is no previous locale pref, then this is the first install 497 // so no need to uninstall first. 498 if (!profile_->GetPrefs()->HasPrefPath(prefs::kHotwordPreviousLanguage)) 499 return false; 500 501 std::string previous_locale = 502 profile_->GetPrefs()->GetString(prefs::kHotwordPreviousLanguage); 503 std::string locale = GetCurrentLocale(profile_); 504 505 // If it's a new locale, then the old extension should be uninstalled. 506 return locale != previous_locale && 507 HotwordService::DoesHotwordSupportLanguage(profile_); 508 } 509