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