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