hotword_service.cc revision f8ee788a64d60abd8f2d742a5fdedde054ecd910
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/plugins/plugin_prefs.h" 16#include "chrome/browser/profiles/profile.h" 17#include "chrome/browser/search/hotword_service_factory.h" 18#include "chrome/common/chrome_paths.h" 19#include "chrome/common/extensions/extension_constants.h" 20#include "chrome/common/pref_names.h" 21#include "content/public/browser/browser_thread.h" 22#include "content/public/browser/notification_service.h" 23#include "content/public/browser/plugin_service.h" 24#include "content/public/common/webplugininfo.h" 25#include "extensions/browser/extension_system.h" 26#include "extensions/common/extension.h" 27#include "grit/generated_resources.h" 28#include "ui/base/l10n/l10n_util.h" 29 30// The whole file relies on the extension systems but this file is built on 31// some non-extension supported platforms and including an API header will cause 32// a compile error since it depends on header files generated by .idl. 33// TODO(mukai): clean up file dependencies and remove this clause. 34#if defined(ENABLE_EXTENSIONS) 35#include "chrome/browser/extensions/api/hotword_private/hotword_private_api.h" 36#endif 37 38#if defined(ENABLE_EXTENSIONS) 39using extensions::BrowserContextKeyedAPIFactory; 40using extensions::HotwordPrivateEventService; 41#endif 42 43namespace { 44 45// Allowed languages for hotwording. 46static const char* kSupportedLocales[] = { 47 "en", 48 "en_us", 49}; 50 51// Enum describing the state of the hotword preference. 52// This is used for UMA stats -- do not reorder or delete items; only add to 53// the end. 54enum HotwordEnabled { 55 UNSET = 0, // The hotword preference has not been set. 56 ENABLED, // The hotword preference is enabled. 57 DISABLED, // The hotword preference is disabled. 58 NUM_HOTWORD_ENABLED_METRICS 59}; 60 61// Enum describing the availability state of the hotword extension. 62// This is used for UMA stats -- do not reorder or delete items; only add to 63// the end. 64enum HotwordExtensionAvailability { 65 UNAVAILABLE = 0, 66 AVAILABLE, 67 PENDING_DOWNLOAD, 68 DISABLED_EXTENSION, 69 NUM_HOTWORD_EXTENSION_AVAILABILITY_METRICS 70}; 71 72// Enum describing the types of errors that can arise when determining 73// if hotwording can be used. NO_ERROR is used so it can be seen how often 74// errors arise relative to when they do not. 75// This is used for UMA stats -- do not reorder or delete items; only add to 76// the end. 77enum HotwordError { 78 NO_HOTWORD_ERROR = 0, 79 GENERIC_HOTWORD_ERROR, 80 NACL_HOTWORD_ERROR, 81 MICROPHONE_HOTWORD_ERROR, 82 NUM_HOTWORD_ERROR_METRICS 83}; 84 85void RecordExtensionAvailabilityMetrics( 86 ExtensionService* service, 87 const extensions::Extension* extension) { 88 HotwordExtensionAvailability availability_state = UNAVAILABLE; 89 if (extension) { 90 availability_state = AVAILABLE; 91 } else if (service->pending_extension_manager() && 92 service->pending_extension_manager()->IsIdPending( 93 extension_misc::kHotwordExtensionId)) { 94 availability_state = PENDING_DOWNLOAD; 95 } else if (!service->IsExtensionEnabled( 96 extension_misc::kHotwordExtensionId)) { 97 availability_state = DISABLED_EXTENSION; 98 } 99 UMA_HISTOGRAM_ENUMERATION("Hotword.HotwordExtensionAvailability", 100 availability_state, 101 NUM_HOTWORD_EXTENSION_AVAILABILITY_METRICS); 102} 103 104void RecordLoggingMetrics(Profile* profile) { 105 // If the user is not opted in to hotword voice search, the audio logging 106 // metric is not valid so it is not recorded. 107 if (!profile->GetPrefs()->GetBoolean(prefs::kHotwordSearchEnabled)) 108 return; 109 110 UMA_HISTOGRAM_BOOLEAN( 111 "Hotword.HotwordAudioLogging", 112 profile->GetPrefs()->GetBoolean(prefs::kHotwordAudioLoggingEnabled)); 113} 114 115void RecordErrorMetrics(int error_message) { 116 HotwordError error = NO_HOTWORD_ERROR; 117 switch (error_message) { 118 case IDS_HOTWORD_GENERIC_ERROR_MESSAGE: 119 error = GENERIC_HOTWORD_ERROR; 120 break; 121 case IDS_HOTWORD_NACL_DISABLED_ERROR_MESSAGE: 122 error = NACL_HOTWORD_ERROR; 123 break; 124 case IDS_HOTWORD_MICROPHONE_ERROR_MESSAGE: 125 error = MICROPHONE_HOTWORD_ERROR; 126 break; 127 default: 128 error = NO_HOTWORD_ERROR; 129 } 130 131 UMA_HISTOGRAM_ENUMERATION("Hotword.HotwordError", 132 error, 133 NUM_HOTWORD_ERROR_METRICS); 134} 135 136ExtensionService* GetExtensionService(Profile* profile) { 137 CHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); 138 139 extensions::ExtensionSystem* extension_system = 140 extensions::ExtensionSystem::Get(profile); 141 if (extension_system) 142 return extension_system->extension_service(); 143 return NULL; 144} 145 146} // namespace 147 148namespace hotword_internal { 149// Constants for the hotword field trial. 150const char kHotwordFieldTrialName[] = "VoiceTrigger"; 151const char kHotwordFieldTrialDisabledGroupName[] = "Disabled"; 152// Old preference constant. 153const char kHotwordUnusablePrefName[] = "hotword.search_enabled"; 154} // namespace hotword_internal 155 156// static 157bool HotwordService::DoesHotwordSupportLanguage(Profile* profile) { 158 std::string locale = 159#if defined(OS_CHROMEOS) 160 // On ChromeOS locale is per-profile. 161 profile->GetPrefs()->GetString(prefs::kApplicationLocale); 162#else 163 g_browser_process->GetApplicationLocale(); 164#endif 165 std::string normalized_locale = l10n_util::NormalizeLocale(locale); 166 StringToLowerASCII(&normalized_locale); 167 168 for (size_t i = 0; i < arraysize(kSupportedLocales); i++) { 169 if (kSupportedLocales[i] == normalized_locale) 170 return true; 171 } 172 return false; 173} 174 175HotwordService::HotwordService(Profile* profile) 176 : profile_(profile), 177 client_(NULL), 178 error_message_(0) { 179 // This will be called during profile initialization which is a good time 180 // to check the user's hotword state. 181 HotwordEnabled enabled_state = UNSET; 182 if (profile_->GetPrefs()->HasPrefPath(prefs::kHotwordSearchEnabled)) { 183 if (profile_->GetPrefs()->GetBoolean(prefs::kHotwordSearchEnabled)) 184 enabled_state = ENABLED; 185 else 186 enabled_state = DISABLED; 187 } else { 188 // If the preference has not been set the hotword extension should 189 // not be running. However, this should only be done if auto-install 190 // is enabled which is gated through the IsHotwordAllowed check. 191 if (IsHotwordAllowed()) 192 DisableHotwordExtension(GetExtensionService(profile_)); 193 } 194 UMA_HISTOGRAM_ENUMERATION("Hotword.Enabled", enabled_state, 195 NUM_HOTWORD_ENABLED_METRICS); 196 197 pref_registrar_.Init(profile_->GetPrefs()); 198 pref_registrar_.Add( 199 prefs::kHotwordSearchEnabled, 200 base::Bind(&HotwordService::OnHotwordSearchEnabledChanged, 201 base::Unretained(this))); 202 203 registrar_.Add(this, 204 chrome::NOTIFICATION_EXTENSION_INSTALLED_DEPRECATED, 205 content::Source<Profile>(profile_)); 206 registrar_.Add(this, 207 chrome::NOTIFICATION_BROWSER_WINDOW_READY, 208 content::NotificationService::AllSources()); 209 210 // Clear the old user pref because it became unusable. 211 // TODO(rlp): Remove this code per crbug.com/358789. 212 if (profile_->GetPrefs()->HasPrefPath( 213 hotword_internal::kHotwordUnusablePrefName)) { 214 profile_->GetPrefs()->ClearPref(hotword_internal::kHotwordUnusablePrefName); 215 } 216} 217 218HotwordService::~HotwordService() { 219} 220 221void HotwordService::Observe(int type, 222 const content::NotificationSource& source, 223 const content::NotificationDetails& details) { 224 if (type == chrome::NOTIFICATION_EXTENSION_INSTALLED_DEPRECATED) { 225 const extensions::Extension* extension = 226 content::Details<const extensions::InstalledExtensionInfo>(details) 227 ->extension; 228 // Disabling the extension automatically on install should only occur 229 // if the user is in the field trial for auto-install which is gated 230 // by the IsHotwordAllowed check. 231 if (IsHotwordAllowed() && 232 extension->id() == extension_misc::kHotwordExtensionId && 233 !profile_->GetPrefs()->GetBoolean(prefs::kHotwordSearchEnabled)) { 234 DisableHotwordExtension(GetExtensionService(profile_)); 235 // Once the extension is disabled, it will not be enabled until the 236 // user opts in at which point the pref registrar will take over 237 // enabling and disabling. 238 registrar_.Remove(this, 239 chrome::NOTIFICATION_EXTENSION_INSTALLED_DEPRECATED, 240 content::Source<Profile>(profile_)); 241 } 242 } else 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 258bool HotwordService::IsServiceAvailable() { 259 error_message_ = 0; 260 261 // Determine if the extension is available. 262 extensions::ExtensionSystem* system = 263 extensions::ExtensionSystem::Get(profile_); 264 ExtensionService* service = system->extension_service(); 265 // Include disabled extensions (true parameter) since it may not be enabled 266 // if the user opted out. 267 const extensions::Extension* extension = 268 service->GetExtensionById(extension_misc::kHotwordExtensionId, true); 269 if (!extension) 270 error_message_ = IDS_HOTWORD_GENERIC_ERROR_MESSAGE; 271 272 RecordExtensionAvailabilityMetrics(service, extension); 273 RecordLoggingMetrics(profile_); 274 275 // NaCl and its associated functions are not available on most mobile 276 // platforms. ENABLE_EXTENSIONS covers those platforms and hey would not 277 // allow Hotwording anyways since it is an extension. 278#if defined(ENABLE_EXTENSIONS) 279 // Determine if NaCl is available. 280 bool nacl_enabled = false; 281 base::FilePath path; 282 if (PathService::Get(chrome::FILE_NACL_PLUGIN, &path)) { 283 content::WebPluginInfo info; 284 PluginPrefs* plugin_prefs = PluginPrefs::GetForProfile(profile_).get(); 285 if (content::PluginService::GetInstance()->GetPluginInfoByPath(path, &info)) 286 nacl_enabled = plugin_prefs->IsPluginEnabled(info); 287 } 288 if (!nacl_enabled) 289 error_message_ = IDS_HOTWORD_NACL_DISABLED_ERROR_MESSAGE; 290#endif 291 292 RecordErrorMetrics(error_message_); 293 294 // Determine if the proper audio capabilities exist. 295 bool audio_capture_allowed = 296 profile_->GetPrefs()->GetBoolean(prefs::kAudioCaptureAllowed); 297 if (!audio_capture_allowed || !HotwordServiceFactory::IsMicrophoneAvailable()) 298 error_message_ = IDS_HOTWORD_MICROPHONE_ERROR_MESSAGE; 299 300 return (error_message_ == 0) && IsHotwordAllowed(); 301} 302 303bool HotwordService::IsHotwordAllowed() { 304 std::string group = base::FieldTrialList::FindFullName( 305 hotword_internal::kHotwordFieldTrialName); 306 return !group.empty() && 307 group != hotword_internal::kHotwordFieldTrialDisabledGroupName && 308 DoesHotwordSupportLanguage(profile_); 309} 310 311bool HotwordService::IsOptedIntoAudioLogging() { 312 // Do not opt the user in if the preference has not been set. 313 return 314 profile_->GetPrefs()->HasPrefPath(prefs::kHotwordAudioLoggingEnabled) && 315 profile_->GetPrefs()->GetBoolean(prefs::kHotwordAudioLoggingEnabled); 316} 317 318void HotwordService::EnableHotwordExtension( 319 ExtensionService* extension_service) { 320 if (extension_service) 321 extension_service->EnableExtension(extension_misc::kHotwordExtensionId); 322} 323 324void HotwordService::DisableHotwordExtension( 325 ExtensionService* extension_service) { 326 if (extension_service) { 327 extension_service->DisableExtension( 328 extension_misc::kHotwordExtensionId, 329 extensions::Extension::DISABLE_USER_ACTION); 330 } 331} 332 333void HotwordService::OnHotwordSearchEnabledChanged( 334 const std::string& pref_name) { 335 DCHECK_EQ(pref_name, std::string(prefs::kHotwordSearchEnabled)); 336 337 ExtensionService* extension_service = GetExtensionService(profile_); 338 if (profile_->GetPrefs()->GetBoolean(prefs::kHotwordSearchEnabled)) 339 EnableHotwordExtension(extension_service); 340 else 341 DisableHotwordExtension(extension_service); 342} 343 344void HotwordService::RequestHotwordSession(HotwordClient* client) { 345#if defined(ENABLE_EXTENSIONS) 346 if (!IsServiceAvailable() || client_) 347 return; 348 349 client_ = client; 350 351 HotwordPrivateEventService* event_service = 352 BrowserContextKeyedAPIFactory<HotwordPrivateEventService>::Get(profile_); 353 if (event_service) 354 event_service->OnHotwordSessionRequested(); 355#endif 356} 357 358void HotwordService::StopHotwordSession(HotwordClient* client) { 359#if defined(ENABLE_EXTENSIONS) 360 if (!IsServiceAvailable()) 361 return; 362 363 DCHECK(client_ == client); 364 365 client_ = NULL; 366 HotwordPrivateEventService* event_service = 367 BrowserContextKeyedAPIFactory<HotwordPrivateEventService>::Get(profile_); 368 if (event_service) 369 event_service->OnHotwordSessionStopped(); 370#endif 371} 372