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