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