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/command_line.h" 8 #include "base/i18n/case_conversion.h" 9 #include "base/metrics/field_trial.h" 10 #include "base/metrics/histogram.h" 11 #include "base/path_service.h" 12 #include "base/prefs/pref_service.h" 13 #include "chrome/browser/browser_process.h" 14 #include "chrome/browser/chrome_notification_types.h" 15 #include "chrome/browser/extensions/api/hotword_private/hotword_private_api.h" 16 #include "chrome/browser/extensions/extension_service.h" 17 #include "chrome/browser/extensions/pending_extension_manager.h" 18 #include "chrome/browser/extensions/updater/extension_updater.h" 19 #include "chrome/browser/extensions/webstore_startup_installer.h" 20 #include "chrome/browser/plugins/plugin_prefs.h" 21 #include "chrome/browser/profiles/profile.h" 22 #include "chrome/browser/search/hotword_service_factory.h" 23 #include "chrome/browser/ui/extensions/application_launch.h" 24 #include "chrome/common/chrome_paths.h" 25 #include "chrome/common/chrome_switches.h" 26 #include "chrome/common/extensions/extension_constants.h" 27 #include "chrome/common/pref_names.h" 28 #include "chrome/grit/generated_resources.h" 29 #include "content/public/browser/browser_thread.h" 30 #include "content/public/browser/notification_service.h" 31 #include "content/public/browser/plugin_service.h" 32 #include "content/public/common/webplugininfo.h" 33 #include "extensions/browser/extension_system.h" 34 #include "extensions/browser/uninstall_reason.h" 35 #include "extensions/common/extension.h" 36 #include "extensions/common/one_shot_event.h" 37 #include "ui/base/l10n/l10n_util.h" 38 39 using extensions::BrowserContextKeyedAPIFactory; 40 using extensions::HotwordPrivateEventService; 41 42 namespace { 43 44 // Allowed languages for hotwording. 45 static const char* kSupportedLocales[] = { 46 "en", 47 "de", 48 "fr", 49 "ru" 50 }; 51 52 // Enum describing the state of the hotword preference. 53 // This is used for UMA stats -- do not reorder or delete items; only add to 54 // the end. 55 enum HotwordEnabled { 56 UNSET = 0, // The hotword preference has not been set. 57 ENABLED, // The hotword preference is enabled. 58 DISABLED, // The hotword preference is disabled. 59 NUM_HOTWORD_ENABLED_METRICS 60 }; 61 62 // Enum describing the availability state of the hotword extension. 63 // This is used for UMA stats -- do not reorder or delete items; only add to 64 // the end. 65 enum HotwordExtensionAvailability { 66 UNAVAILABLE = 0, 67 AVAILABLE, 68 PENDING_DOWNLOAD, 69 DISABLED_EXTENSION, 70 NUM_HOTWORD_EXTENSION_AVAILABILITY_METRICS 71 }; 72 73 // Enum describing the types of errors that can arise when determining 74 // if hotwording can be used. NO_ERROR is used so it can be seen how often 75 // errors arise relative to when they do not. 76 // This is used for UMA stats -- do not reorder or delete items; only add to 77 // the end. 78 enum HotwordError { 79 NO_HOTWORD_ERROR = 0, 80 GENERIC_HOTWORD_ERROR, 81 NACL_HOTWORD_ERROR, 82 MICROPHONE_HOTWORD_ERROR, 83 NUM_HOTWORD_ERROR_METRICS 84 }; 85 86 void RecordExtensionAvailabilityMetrics( 87 ExtensionService* service, 88 const extensions::Extension* extension) { 89 HotwordExtensionAvailability availability_state = UNAVAILABLE; 90 if (extension) { 91 availability_state = AVAILABLE; 92 } else if (service->pending_extension_manager() && 93 service->pending_extension_manager()->IsIdPending( 94 extension_misc::kHotwordExtensionId)) { 95 availability_state = PENDING_DOWNLOAD; 96 } else if (!service->IsExtensionEnabled( 97 extension_misc::kHotwordExtensionId)) { 98 availability_state = DISABLED_EXTENSION; 99 } 100 UMA_HISTOGRAM_ENUMERATION("Hotword.HotwordExtensionAvailability", 101 availability_state, 102 NUM_HOTWORD_EXTENSION_AVAILABILITY_METRICS); 103 } 104 105 void RecordLoggingMetrics(Profile* profile) { 106 // If the user is not opted in to hotword voice search, the audio logging 107 // metric is not valid so it is not recorded. 108 if (!profile->GetPrefs()->GetBoolean(prefs::kHotwordSearchEnabled)) 109 return; 110 111 UMA_HISTOGRAM_BOOLEAN( 112 "Hotword.HotwordAudioLogging", 113 profile->GetPrefs()->GetBoolean(prefs::kHotwordAudioLoggingEnabled)); 114 } 115 116 void RecordErrorMetrics(int error_message) { 117 HotwordError error = NO_HOTWORD_ERROR; 118 switch (error_message) { 119 case IDS_HOTWORD_GENERIC_ERROR_MESSAGE: 120 error = GENERIC_HOTWORD_ERROR; 121 break; 122 case IDS_HOTWORD_NACL_DISABLED_ERROR_MESSAGE: 123 error = NACL_HOTWORD_ERROR; 124 break; 125 case IDS_HOTWORD_MICROPHONE_ERROR_MESSAGE: 126 error = MICROPHONE_HOTWORD_ERROR; 127 break; 128 default: 129 error = NO_HOTWORD_ERROR; 130 } 131 132 UMA_HISTOGRAM_ENUMERATION("Hotword.HotwordError", 133 error, 134 NUM_HOTWORD_ERROR_METRICS); 135 } 136 137 ExtensionService* GetExtensionService(Profile* profile) { 138 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); 139 140 extensions::ExtensionSystem* extension_system = 141 extensions::ExtensionSystem::Get(profile); 142 return extension_system ? extension_system->extension_service() : NULL; 143 } 144 145 std::string GetCurrentLocale(Profile* profile) { 146 #if defined(OS_CHROMEOS) 147 std::string profile_locale = 148 profile->GetPrefs()->GetString(prefs::kApplicationLocale); 149 if (!profile_locale.empty()) { 150 // On ChromeOS locale is per-profile, but only if set. 151 return profile_locale; 152 } 153 #endif 154 return g_browser_process->GetApplicationLocale(); 155 } 156 157 } // namespace 158 159 namespace hotword_internal { 160 // Constants for the hotword field trial. 161 const char kHotwordFieldTrialName[] = "VoiceTrigger"; 162 const char kHotwordFieldTrialDisabledGroupName[] = "Disabled"; 163 // Old preference constant. 164 const char kHotwordUnusablePrefName[] = "hotword.search_enabled"; 165 } // namespace hotword_internal 166 167 // static 168 bool HotwordService::DoesHotwordSupportLanguage(Profile* profile) { 169 std::string normalized_locale = 170 l10n_util::NormalizeLocale(GetCurrentLocale(profile)); 171 base::StringToLowerASCII(&normalized_locale); 172 173 for (size_t i = 0; i < arraysize(kSupportedLocales); i++) { 174 if (normalized_locale.compare(0, 2, kSupportedLocales[i]) == 0) 175 return true; 176 } 177 return false; 178 } 179 180 // static 181 bool HotwordService::IsExperimentalHotwordingEnabled() { 182 CommandLine* command_line = CommandLine::ForCurrentProcess(); 183 return command_line->HasSwitch(switches::kEnableExperimentalHotwording); 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 extensions::UninstallReason reason) { 262 CHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); 263 264 if (extension->id() != extension_misc::kHotwordExtensionId || 265 profile_ != Profile::FromBrowserContext(browser_context) || 266 !GetExtensionService(profile_)) 267 return; 268 269 // If the extension wasn't uninstalled due to language change, don't try to 270 // reinstall it. 271 if (!reinstall_pending_) 272 return; 273 274 InstallHotwordExtensionFromWebstore(); 275 SetPreviousLanguagePref(); 276 } 277 278 void HotwordService::InstallHotwordExtensionFromWebstore() { 279 installer_ = new extensions::WebstoreStartupInstaller( 280 extension_misc::kHotwordExtensionId, 281 profile_, 282 false, 283 extensions::WebstoreStandaloneInstaller::Callback()); 284 installer_->BeginInstall(); 285 } 286 287 void HotwordService::OnExtensionInstalled( 288 content::BrowserContext* browser_context, 289 const extensions::Extension* extension, 290 bool is_update) { 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, 366 extensions::UNINSTALL_REASON_INTERNAL_MANAGEMENT, 367 base::Bind(&base::DoNothing), 368 &error)) { 369 LOG(WARNING) << "Cannot uninstall extension with id " 370 << extension_misc::kHotwordExtensionId 371 << ": " << error; 372 reinstall_pending_ = false; 373 return false; 374 } 375 return true; 376 } 377 378 bool HotwordService::IsServiceAvailable() { 379 error_message_ = 0; 380 381 // Determine if the extension is available. 382 extensions::ExtensionSystem* system = 383 extensions::ExtensionSystem::Get(profile_); 384 ExtensionService* service = system->extension_service(); 385 // Include disabled extensions (true parameter) since it may not be enabled 386 // if the user opted out. 387 std::string extensionId; 388 if (IsExperimentalHotwordingEnabled()) { 389 // TODO(amistry): Handle reloading on language change as the old extension 390 // does. 391 extensionId = extension_misc::kHotwordSharedModuleId; 392 } else { 393 extensionId = extension_misc::kHotwordExtensionId; 394 } 395 const extensions::Extension* extension = 396 service->GetExtensionById(extensionId, true); 397 if (!extension) 398 error_message_ = IDS_HOTWORD_GENERIC_ERROR_MESSAGE; 399 400 RecordExtensionAvailabilityMetrics(service, extension); 401 RecordLoggingMetrics(profile_); 402 403 // Determine if NaCl is available. 404 bool nacl_enabled = false; 405 base::FilePath path; 406 if (PathService::Get(chrome::FILE_NACL_PLUGIN, &path)) { 407 content::WebPluginInfo info; 408 PluginPrefs* plugin_prefs = PluginPrefs::GetForProfile(profile_).get(); 409 if (content::PluginService::GetInstance()->GetPluginInfoByPath(path, &info)) 410 nacl_enabled = plugin_prefs->IsPluginEnabled(info); 411 } 412 if (!nacl_enabled) 413 error_message_ = IDS_HOTWORD_NACL_DISABLED_ERROR_MESSAGE; 414 415 RecordErrorMetrics(error_message_); 416 417 // Determine if the proper audio capabilities exist. 418 bool audio_capture_allowed = 419 profile_->GetPrefs()->GetBoolean(prefs::kAudioCaptureAllowed); 420 if (!audio_capture_allowed || !HotwordServiceFactory::IsMicrophoneAvailable()) 421 error_message_ = IDS_HOTWORD_MICROPHONE_ERROR_MESSAGE; 422 423 return (error_message_ == 0) && IsHotwordAllowed(); 424 } 425 426 bool HotwordService::IsHotwordAllowed() { 427 std::string group = base::FieldTrialList::FindFullName( 428 hotword_internal::kHotwordFieldTrialName); 429 return !group.empty() && 430 group != hotword_internal::kHotwordFieldTrialDisabledGroupName && 431 DoesHotwordSupportLanguage(profile_); 432 } 433 434 bool HotwordService::IsOptedIntoAudioLogging() { 435 // Do not opt the user in if the preference has not been set. 436 return 437 profile_->GetPrefs()->HasPrefPath(prefs::kHotwordAudioLoggingEnabled) && 438 profile_->GetPrefs()->GetBoolean(prefs::kHotwordAudioLoggingEnabled); 439 } 440 441 void HotwordService::EnableHotwordExtension( 442 ExtensionService* extension_service) { 443 if (extension_service) 444 extension_service->EnableExtension(extension_misc::kHotwordExtensionId); 445 } 446 447 void HotwordService::DisableHotwordExtension( 448 ExtensionService* extension_service) { 449 if (extension_service) { 450 extension_service->DisableExtension( 451 extension_misc::kHotwordExtensionId, 452 extensions::Extension::DISABLE_USER_ACTION); 453 } 454 } 455 456 void HotwordService::LaunchHotwordAudioVerificationApp( 457 const LaunchMode& launch_mode) { 458 hotword_audio_verification_launch_mode_ = launch_mode; 459 460 ExtensionService* extension_service = GetExtensionService(profile_); 461 if (!extension_service) 462 return; 463 const extensions::Extension* extension = extension_service->GetExtensionById( 464 extension_misc::kHotwordAudioVerificationAppId, true); 465 if (!extension) 466 return; 467 468 OpenApplication(AppLaunchParams( 469 profile_, extension, extensions::LAUNCH_CONTAINER_WINDOW, NEW_WINDOW)); 470 } 471 472 HotwordService::LaunchMode 473 HotwordService::GetHotwordAudioVerificationLaunchMode() { 474 return hotword_audio_verification_launch_mode_; 475 } 476 477 void HotwordService::OnHotwordSearchEnabledChanged( 478 const std::string& pref_name) { 479 DCHECK_EQ(pref_name, std::string(prefs::kHotwordSearchEnabled)); 480 481 ExtensionService* extension_service = GetExtensionService(profile_); 482 if (profile_->GetPrefs()->GetBoolean(prefs::kHotwordSearchEnabled)) 483 EnableHotwordExtension(extension_service); 484 else 485 DisableHotwordExtension(extension_service); 486 } 487 488 void HotwordService::RequestHotwordSession(HotwordClient* client) { 489 if (!IsServiceAvailable() || (client_ && client_ != client)) 490 return; 491 492 client_ = client; 493 494 HotwordPrivateEventService* event_service = 495 BrowserContextKeyedAPIFactory<HotwordPrivateEventService>::Get(profile_); 496 if (event_service) 497 event_service->OnHotwordSessionRequested(); 498 } 499 500 void HotwordService::StopHotwordSession(HotwordClient* client) { 501 if (!IsServiceAvailable()) 502 return; 503 504 DCHECK(client_ == client); 505 506 client_ = NULL; 507 HotwordPrivateEventService* event_service = 508 BrowserContextKeyedAPIFactory<HotwordPrivateEventService>::Get(profile_); 509 if (event_service) 510 event_service->OnHotwordSessionStopped(); 511 } 512 513 void HotwordService::SetPreviousLanguagePref() { 514 profile_->GetPrefs()->SetString(prefs::kHotwordPreviousLanguage, 515 GetCurrentLocale(profile_)); 516 } 517 518 bool HotwordService::ShouldReinstallHotwordExtension() { 519 // If there is no previous locale pref, then this is the first install 520 // so no need to uninstall first. 521 if (!profile_->GetPrefs()->HasPrefPath(prefs::kHotwordPreviousLanguage)) 522 return false; 523 524 std::string previous_locale = 525 profile_->GetPrefs()->GetString(prefs::kHotwordPreviousLanguage); 526 std::string locale = GetCurrentLocale(profile_); 527 528 // If it's a new locale, then the old extension should be uninstalled. 529 return locale != previous_locale && 530 HotwordService::DoesHotwordSupportLanguage(profile_); 531 } 532