hotword_service.cc revision 0529e5d033099cbfc42635f6f6183833b09dff6e
15d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)// Copyright 2013 The Chromium Authors. All rights reserved. 22a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)// Use of this source code is governed by a BSD-style license that can be 32a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)// found in the LICENSE file. 42a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 52a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#include "chrome/browser/search/hotword_service.h" 62a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 72a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#include "base/i18n/case_conversion.h" 82a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#include "base/metrics/field_trial.h" 92a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#include "base/metrics/histogram.h" 102a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#include "base/prefs/pref_service.h" 11c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)#include "chrome/browser/browser_process.h" 122a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#include "chrome/browser/chrome_notification_types.h" 132a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#include "chrome/browser/extensions/extension_service.h" 142a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#include "chrome/browser/profiles/profile.h" 152a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#include "chrome/common/extensions/extension_constants.h" 162a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#include "chrome/common/pref_names.h" 172a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#include "content/public/browser/browser_thread.h" 182a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#include "content/public/browser/notification_service.h" 192a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#include "extensions/browser/extension_system.h" 202a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#include "extensions/common/extension.h" 212a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#include "ui/base/l10n/l10n_util.h" 222a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 232a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)namespace { 242a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)const int kMaxTimesToShowOptInPopup = 10; 252a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 262a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)// Allowed languages for hotwording. 272a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)static const char* kSupportedLocales[] = { 282a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) "en", 292a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) "en_us" 3023730a6e56a168d1879203e4b3819bb36e3d8f1fTorne (Richard Coles)}; 312a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 322a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)// Enum describing the state of the hotword preference. 332a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)// This is used for UMA stats -- do not reorder or delete items; only add to 342a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)// the end. 352a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)enum HotwordEnabled { 362a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) UNSET = 0, // The hotword preference has not been set. 372a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) ENABLED, // The hotword preference is enabled. 382a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) DISABLED, // The hotword preference is disabled. 392a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) NUM_HOTWORD_ENABLED_METRICS 402a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)}; 4158e6fbe4ee35d65e14b626c557d37565bf8ad179Ben Murdoch 4258e6fbe4ee35d65e14b626c557d37565bf8ad179Ben Murdoch// Enum describing the availability state of the hotword extension. 432a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)// This is used for UMA stats -- do not reorder or delete items; only add to 442a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)// the end. 452a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)enum HotwordExtensionAvailability { 462a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) UNAVAILABLE = 0, 472a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) AVAILABLE, 482a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) PENDING_DOWNLOAD, 492a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) DISABLED_EXTENSION, 502a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) NUM_HOTWORD_EXTENSION_AVAILABILITY_METRICS 512a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)}; 522a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 532a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)void RecordAvailabilityMetrics( 542a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) ExtensionService* service, 552a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) const extensions::Extension* extension) { 562a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) HotwordExtensionAvailability availability_state = UNAVAILABLE; 572a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) if (extension) { 582a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) availability_state = AVAILABLE; 592a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) } else if (service->pending_extension_manager() && 602a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) service->pending_extension_manager()->IsIdPending( 612a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) extension_misc::kHotwordExtensionId)) { 622a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) availability_state = PENDING_DOWNLOAD; 632a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) } else if (!service->IsExtensionEnabled( 642a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) extension_misc::kHotwordExtensionId)) { 652a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) availability_state = DISABLED_EXTENSION; 662a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) } 672a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) UMA_HISTOGRAM_ENUMERATION("Hotword.HotwordExtensionAvailability", 682a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) availability_state, 692a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) NUM_HOTWORD_EXTENSION_AVAILABILITY_METRICS); 702a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)} 712a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 722a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)void RecordLoggingMetrics(Profile* profile) { 732a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // If the user is not opted in to hotword voice search, the audio logging 742a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // metric is not valid so it is not recorded. 752a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) if (!profile->GetPrefs()->GetBoolean(prefs::kHotwordSearchEnabled)) 762a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) return; 772a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 782a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) UMA_HISTOGRAM_BOOLEAN( 792a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) "Hotword.HotwordAudioLogging", 802a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) profile->GetPrefs()->GetBoolean(prefs::kHotwordAudioLoggingEnabled)); 812a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)} 822a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 832a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)ExtensionService* GetExtensionService(Profile* profile) { 842a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) CHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); 852a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 862a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) extensions::ExtensionSystem* extension_system = 872a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) extensions::ExtensionSystem::Get(profile); 882a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) if (extension_system) 892a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) return extension_system->extension_service(); 902a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) return NULL; 912a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)} 922a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 932a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)} // namespace 942a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 952a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)namespace hotword_internal { 962a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)// Constants for the hotword field trial. 972a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)const char kHotwordFieldTrialName[] = "VoiceTrigger"; 982a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)const char kHotwordFieldTrialDisabledGroupName[] = "Disabled"; 992a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)// Old preference constant. 1002a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)const char kHotwordUnusablePrefName[] = "hotword.search_enabled"; 1012a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)} // namespace hotword_internal 1022a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 1032a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)// static 1042a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)bool HotwordService::DoesHotwordSupportLanguage(Profile* profile) { 1052a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) std::string locale = 106c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)#if defined(OS_CHROMEOS) 107c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) // On ChromeOS locale is per-profile. 108c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) profile->GetPrefs()->GetString(prefs::kApplicationLocale); 109c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)#else 110c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) g_browser_process->GetApplicationLocale(); 1112a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#endif 112 std::string normalized_locale = l10n_util::NormalizeLocale(locale); 113 StringToLowerASCII(&normalized_locale); 114 115 for (size_t i = 0; i < arraysize(kSupportedLocales); i++) { 116 if (kSupportedLocales[i] == normalized_locale) 117 return true; 118 } 119 return false; 120} 121 122HotwordService::HotwordService(Profile* profile) 123 : profile_(profile) { 124 // This will be called during profile initialization which is a good time 125 // to check the user's hotword state. 126 HotwordEnabled enabled_state = UNSET; 127 if (profile_->GetPrefs()->HasPrefPath(prefs::kHotwordSearchEnabled)) { 128 if (profile_->GetPrefs()->GetBoolean(prefs::kHotwordSearchEnabled)) 129 enabled_state = ENABLED; 130 else 131 enabled_state = DISABLED; 132 } else { 133 // If the preference has not been set the hotword extension should 134 // not be running. However, this should only be done if auto-install 135 // is enabled which is gated through the IsHotwordAllowed check. 136 if (IsHotwordAllowed()) 137 DisableHotwordExtension(GetExtensionService(profile_)); 138 } 139 UMA_HISTOGRAM_ENUMERATION("Hotword.Enabled", enabled_state, 140 NUM_HOTWORD_ENABLED_METRICS); 141 142 pref_registrar_.Init(profile_->GetPrefs()); 143 pref_registrar_.Add( 144 prefs::kHotwordSearchEnabled, 145 base::Bind(&HotwordService::OnHotwordSearchEnabledChanged, 146 base::Unretained(this))); 147 148 registrar_.Add(this, 149 chrome::NOTIFICATION_EXTENSION_INSTALLED, 150 content::Source<Profile>(profile_)); 151 152 // Clear the old user pref because it became unusable. 153 // TODO(rlp): Remove this code per crbug.com/358789. 154 if (profile_->GetPrefs()->HasPrefPath( 155 hotword_internal::kHotwordUnusablePrefName)) { 156 profile_->GetPrefs()->ClearPref(hotword_internal::kHotwordUnusablePrefName); 157 } 158} 159 160HotwordService::~HotwordService() { 161} 162 163void HotwordService::Observe(int type, 164 const content::NotificationSource& source, 165 const content::NotificationDetails& details) { 166 if (type == chrome::NOTIFICATION_EXTENSION_INSTALLED) { 167 const extensions::Extension* extension = 168 content::Details<const extensions::InstalledExtensionInfo>(details) 169 ->extension; 170 // Disabling the extension automatically on install should only occur 171 // if the user is in the field trial for auto-install which is gated 172 // by the IsHotwordAllowed check. 173 if (IsHotwordAllowed() && 174 extension->id() == extension_misc::kHotwordExtensionId && 175 !profile_->GetPrefs()->GetBoolean(prefs::kHotwordSearchEnabled)) { 176 DisableHotwordExtension(GetExtensionService(profile_)); 177 // Once the extension is disabled, it will not be enabled until the 178 // user opts in at which point the pref registrar will take over 179 // enabling and disabling. 180 registrar_.Remove(this, 181 chrome::NOTIFICATION_EXTENSION_INSTALLED, 182 content::Source<Profile>(profile_)); 183 } 184 } 185} 186 187bool HotwordService::ShouldShowOptInPopup() { 188 if (profile_->IsOffTheRecord()) 189 return false; 190 191 // Profile is not off the record. 192 if (profile_->GetPrefs()->HasPrefPath(prefs::kHotwordSearchEnabled)) 193 return false; // Already opted in or opted out; 194 195 int number_shown = profile_->GetPrefs()->GetInteger( 196 prefs::kHotwordOptInPopupTimesShown); 197 return number_shown < MaxNumberTimesToShowOptInPopup(); 198} 199 200int HotwordService::MaxNumberTimesToShowOptInPopup() { 201 return kMaxTimesToShowOptInPopup; 202} 203 204void HotwordService::ShowOptInPopup() { 205 int number_shown = profile_->GetPrefs()->GetInteger( 206 prefs::kHotwordOptInPopupTimesShown); 207 profile_->GetPrefs()->SetInteger(prefs::kHotwordOptInPopupTimesShown, 208 ++number_shown); 209 // TODO(rlp): actually show opt in popup when linked up to extension. 210} 211 212bool HotwordService::IsServiceAvailable() { 213 extensions::ExtensionSystem* system = 214 extensions::ExtensionSystem::Get(profile_); 215 ExtensionService* service = system->extension_service(); 216 // Include disabled extensions (true parameter) since it may not be enabled 217 // if the user opted out. 218 const extensions::Extension* extension = 219 service->GetExtensionById(extension_misc::kHotwordExtensionId, true); 220 221 RecordAvailabilityMetrics(service, extension); 222 RecordLoggingMetrics(profile_); 223 224 return extension && IsHotwordAllowed(); 225} 226 227bool HotwordService::IsHotwordAllowed() { 228 std::string group = base::FieldTrialList::FindFullName( 229 hotword_internal::kHotwordFieldTrialName); 230 return !group.empty() && 231 group != hotword_internal::kHotwordFieldTrialDisabledGroupName && 232 DoesHotwordSupportLanguage(profile_); 233} 234 235bool HotwordService::IsOptedIntoAudioLogging() { 236 // Do not opt the user in if the preference has not been set. 237 return 238 profile_->GetPrefs()->HasPrefPath(prefs::kHotwordAudioLoggingEnabled) && 239 profile_->GetPrefs()->GetBoolean(prefs::kHotwordAudioLoggingEnabled); 240} 241 242bool HotwordService::RetryHotwordExtension() { 243 ExtensionService* extension_service = GetExtensionService(profile_); 244 if (!extension_service) 245 return false; 246 247 extension_service->ReloadExtension(extension_misc::kHotwordExtensionId); 248 return true; 249} 250 251void HotwordService::EnableHotwordExtension( 252 ExtensionService* extension_service) { 253 if (extension_service) 254 extension_service->EnableExtension(extension_misc::kHotwordExtensionId); 255} 256 257void HotwordService::DisableHotwordExtension( 258 ExtensionService* extension_service) { 259 if (extension_service) { 260 extension_service->DisableExtension( 261 extension_misc::kHotwordExtensionId, 262 extensions::Extension::DISABLE_USER_ACTION); 263 } 264} 265 266void HotwordService::OnHotwordSearchEnabledChanged( 267 const std::string& pref_name) { 268 DCHECK_EQ(pref_name, std::string(prefs::kHotwordSearchEnabled)); 269 270 ExtensionService* extension_service = GetExtensionService(profile_); 271 if (profile_->GetPrefs()->GetBoolean(prefs::kHotwordSearchEnabled)) 272 EnableHotwordExtension(extension_service); 273 else 274 DisableHotwordExtension(extension_service); 275} 276