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