hotword_service.cc revision 6e8cce623b6e4fe0c9e4af605d675dd9d0338c38
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/api/hotword_private/hotword_private_api.h"
15#include "chrome/browser/extensions/extension_service.h"
16#include "chrome/browser/extensions/pending_extension_manager.h"
17#include "chrome/browser/extensions/updater/extension_updater.h"
18#include "chrome/browser/extensions/webstore_startup_installer.h"
19#include "chrome/browser/plugins/plugin_prefs.h"
20#include "chrome/browser/profiles/profile.h"
21#include "chrome/browser/search/hotword_service_factory.h"
22#include "chrome/common/chrome_paths.h"
23#include "chrome/common/extensions/extension_constants.h"
24#include "chrome/common/pref_names.h"
25#include "content/public/browser/browser_thread.h"
26#include "content/public/browser/notification_service.h"
27#include "content/public/browser/plugin_service.h"
28#include "content/public/common/webplugininfo.h"
29#include "extensions/browser/extension_system.h"
30#include "extensions/browser/uninstall_reason.h"
31#include "extensions/common/extension.h"
32#include "extensions/common/one_shot_event.h"
33#include "grit/generated_resources.h"
34#include "ui/base/l10n/l10n_util.h"
35
36using extensions::BrowserContextKeyedAPIFactory;
37using extensions::HotwordPrivateEventService;
38
39namespace {
40
41// Allowed languages for hotwording.
42static const char* kSupportedLocales[] = {
43  "en",
44  "de",
45  "fr",
46  "ru"
47};
48
49// Enum describing the state of the hotword preference.
50// This is used for UMA stats -- do not reorder or delete items; only add to
51// the end.
52enum HotwordEnabled {
53  UNSET = 0,  // The hotword preference has not been set.
54  ENABLED,    // The hotword preference is enabled.
55  DISABLED,   // The hotword preference is disabled.
56  NUM_HOTWORD_ENABLED_METRICS
57};
58
59// Enum describing the availability state of the hotword extension.
60// This is used for UMA stats -- do not reorder or delete items; only add to
61// the end.
62enum HotwordExtensionAvailability {
63  UNAVAILABLE = 0,
64  AVAILABLE,
65  PENDING_DOWNLOAD,
66  DISABLED_EXTENSION,
67  NUM_HOTWORD_EXTENSION_AVAILABILITY_METRICS
68};
69
70// Enum describing the types of errors that can arise when determining
71// if hotwording can be used. NO_ERROR is used so it can be seen how often
72// errors arise relative to when they do not.
73// This is used for UMA stats -- do not reorder or delete items; only add to
74// the end.
75enum HotwordError {
76  NO_HOTWORD_ERROR = 0,
77  GENERIC_HOTWORD_ERROR,
78  NACL_HOTWORD_ERROR,
79  MICROPHONE_HOTWORD_ERROR,
80  NUM_HOTWORD_ERROR_METRICS
81};
82
83void RecordExtensionAvailabilityMetrics(
84    ExtensionService* service,
85    const extensions::Extension* extension) {
86  HotwordExtensionAvailability availability_state = UNAVAILABLE;
87  if (extension) {
88    availability_state = AVAILABLE;
89  } else if (service->pending_extension_manager() &&
90             service->pending_extension_manager()->IsIdPending(
91                 extension_misc::kHotwordExtensionId)) {
92    availability_state = PENDING_DOWNLOAD;
93  } else if (!service->IsExtensionEnabled(
94      extension_misc::kHotwordExtensionId)) {
95    availability_state = DISABLED_EXTENSION;
96  }
97  UMA_HISTOGRAM_ENUMERATION("Hotword.HotwordExtensionAvailability",
98                            availability_state,
99                            NUM_HOTWORD_EXTENSION_AVAILABILITY_METRICS);
100}
101
102void RecordLoggingMetrics(Profile* profile) {
103  // If the user is not opted in to hotword voice search, the audio logging
104  // metric is not valid so it is not recorded.
105  if (!profile->GetPrefs()->GetBoolean(prefs::kHotwordSearchEnabled))
106    return;
107
108  UMA_HISTOGRAM_BOOLEAN(
109      "Hotword.HotwordAudioLogging",
110      profile->GetPrefs()->GetBoolean(prefs::kHotwordAudioLoggingEnabled));
111}
112
113void RecordErrorMetrics(int error_message) {
114  HotwordError error = NO_HOTWORD_ERROR;
115  switch (error_message) {
116    case IDS_HOTWORD_GENERIC_ERROR_MESSAGE:
117      error = GENERIC_HOTWORD_ERROR;
118      break;
119    case IDS_HOTWORD_NACL_DISABLED_ERROR_MESSAGE:
120      error = NACL_HOTWORD_ERROR;
121      break;
122    case IDS_HOTWORD_MICROPHONE_ERROR_MESSAGE:
123      error = MICROPHONE_HOTWORD_ERROR;
124      break;
125    default:
126      error = NO_HOTWORD_ERROR;
127  }
128
129  UMA_HISTOGRAM_ENUMERATION("Hotword.HotwordError",
130                            error,
131                            NUM_HOTWORD_ERROR_METRICS);
132}
133
134ExtensionService* GetExtensionService(Profile* profile) {
135  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
136
137  extensions::ExtensionSystem* extension_system =
138      extensions::ExtensionSystem::Get(profile);
139  return extension_system ?  extension_system->extension_service() : NULL;
140}
141
142std::string GetCurrentLocale(Profile* profile) {
143  std::string locale =
144#if defined(OS_CHROMEOS)
145      // On ChromeOS locale is per-profile.
146      profile->GetPrefs()->GetString(prefs::kApplicationLocale);
147#else
148      g_browser_process->GetApplicationLocale();
149#endif
150  return locale;
151}
152
153}  // namespace
154
155namespace hotword_internal {
156// Constants for the hotword field trial.
157const char kHotwordFieldTrialName[] = "VoiceTrigger";
158const char kHotwordFieldTrialDisabledGroupName[] = "Disabled";
159// Old preference constant.
160const char kHotwordUnusablePrefName[] = "hotword.search_enabled";
161}  // namespace hotword_internal
162
163// static
164bool HotwordService::DoesHotwordSupportLanguage(Profile* profile) {
165  std::string normalized_locale =
166      l10n_util::NormalizeLocale(GetCurrentLocale(profile));
167  base::StringToLowerASCII(&normalized_locale);
168
169  for (size_t i = 0; i < arraysize(kSupportedLocales); i++) {
170    if (normalized_locale.compare(0, 2, kSupportedLocales[i]) == 0)
171      return true;
172  }
173  return false;
174}
175
176HotwordService::HotwordService(Profile* profile)
177    : profile_(profile),
178      extension_registry_observer_(this),
179      client_(NULL),
180      error_message_(0),
181      reinstall_pending_(false),
182      weak_factory_(this) {
183  extension_registry_observer_.Add(extensions::ExtensionRegistry::Get(profile));
184  // This will be called during profile initialization which is a good time
185  // to check the user's hotword state.
186  HotwordEnabled enabled_state = UNSET;
187  if (profile_->GetPrefs()->HasPrefPath(prefs::kHotwordSearchEnabled)) {
188    if (profile_->GetPrefs()->GetBoolean(prefs::kHotwordSearchEnabled))
189      enabled_state = ENABLED;
190    else
191      enabled_state = DISABLED;
192  } else {
193    // If the preference has not been set the hotword extension should
194    // not be running. However, this should only be done if auto-install
195    // is enabled which is gated through the IsHotwordAllowed check.
196    if (IsHotwordAllowed())
197      DisableHotwordExtension(GetExtensionService(profile_));
198  }
199  UMA_HISTOGRAM_ENUMERATION("Hotword.Enabled", enabled_state,
200                            NUM_HOTWORD_ENABLED_METRICS);
201
202  pref_registrar_.Init(profile_->GetPrefs());
203  pref_registrar_.Add(
204      prefs::kHotwordSearchEnabled,
205      base::Bind(&HotwordService::OnHotwordSearchEnabledChanged,
206                 base::Unretained(this)));
207
208  registrar_.Add(this,
209                 chrome::NOTIFICATION_BROWSER_WINDOW_READY,
210                 content::NotificationService::AllSources());
211
212  extensions::ExtensionSystem::Get(profile_)->ready().Post(
213      FROM_HERE,
214      base::Bind(base::IgnoreResult(
215          &HotwordService::MaybeReinstallHotwordExtension),
216                 weak_factory_.GetWeakPtr()));
217
218  // Clear the old user pref because it became unusable.
219  // TODO(rlp): Remove this code per crbug.com/358789.
220  if (profile_->GetPrefs()->HasPrefPath(
221          hotword_internal::kHotwordUnusablePrefName)) {
222    profile_->GetPrefs()->ClearPref(hotword_internal::kHotwordUnusablePrefName);
223  }
224}
225
226HotwordService::~HotwordService() {
227}
228
229void HotwordService::Observe(int type,
230                             const content::NotificationSource& source,
231                             const content::NotificationDetails& details) {
232  if (type == chrome::NOTIFICATION_BROWSER_WINDOW_READY) {
233    // The microphone monitor must be initialized as the page is loading
234    // so that the state of the microphone is available when the page
235    // loads. The Ok Google Hotword setting will display an error if there
236    // is no microphone but this information will not be up-to-date unless
237    // the monitor had already been started. Furthermore, the pop up to
238    // opt in to hotwording won't be available if it thinks there is no
239    // microphone. There is no hard guarantee that the monitor will actually
240    // be up by the time it's needed, but this is the best we can do without
241    // starting it at start up which slows down start up too much.
242    // The content/media for microphone uses the same observer design and
243    // makes use of the same audio device monitor.
244    HotwordServiceFactory::GetInstance()->UpdateMicrophoneState();
245  }
246}
247
248void HotwordService::OnExtensionUninstalled(
249    content::BrowserContext* browser_context,
250    const extensions::Extension* extension,
251    extensions::UninstallReason reason) {
252  CHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
253
254  if (extension->id() != extension_misc::kHotwordExtensionId ||
255      profile_ != Profile::FromBrowserContext(browser_context) ||
256      !GetExtensionService(profile_))
257    return;
258
259  // If the extension wasn't uninstalled due to language change, don't try to
260  // reinstall it.
261  if (!reinstall_pending_)
262    return;
263
264  InstallHotwordExtensionFromWebstore();
265  SetPreviousLanguagePref();
266}
267
268void HotwordService::InstallHotwordExtensionFromWebstore() {
269  installer_ = new extensions::WebstoreStartupInstaller(
270      extension_misc::kHotwordExtensionId,
271      profile_,
272      false,
273      extensions::WebstoreStandaloneInstaller::Callback());
274  installer_->BeginInstall();
275}
276
277void HotwordService::OnExtensionInstalled(
278    content::BrowserContext* browser_context,
279    const extensions::Extension* extension,
280    bool is_update) {
281
282  if (extension->id() != extension_misc::kHotwordExtensionId ||
283      profile_ != Profile::FromBrowserContext(browser_context))
284    return;
285
286  // If the previous locale pref has never been set, set it now since
287  // the extension has been installed.
288  if (!profile_->GetPrefs()->HasPrefPath(prefs::kHotwordPreviousLanguage))
289    SetPreviousLanguagePref();
290
291  // If MaybeReinstallHotwordExtension already triggered an uninstall, we
292  // don't want to loop and trigger another uninstall-install cycle.
293  // However, if we arrived here via an uninstall-triggered-install (and in
294  // that case |reinstall_pending_| will be true) then we know install
295  // has completed and we can reset |reinstall_pending_|.
296  if (!reinstall_pending_)
297    MaybeReinstallHotwordExtension();
298  else
299    reinstall_pending_ = false;
300
301  // Now that the extension is installed, if the user has not selected
302  // the preference on, make sure it is turned off.
303  //
304  // Disabling the extension automatically on install should only occur
305  // if the user is in the field trial for auto-install which is gated
306  // by the IsHotwordAllowed check. The check for IsHotwordAllowed() here
307  // can be removed once it's known that few people have manually
308  // installed extension.
309  if (IsHotwordAllowed() &&
310      !profile_->GetPrefs()->GetBoolean(prefs::kHotwordSearchEnabled)) {
311    DisableHotwordExtension(GetExtensionService(profile_));
312  }
313}
314
315bool HotwordService::MaybeReinstallHotwordExtension() {
316  CHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
317
318  ExtensionService* extension_service = GetExtensionService(profile_);
319  if (!extension_service)
320    return false;
321
322  const extensions::Extension* extension = extension_service->GetExtensionById(
323      extension_misc::kHotwordExtensionId, true);
324  if (!extension)
325    return false;
326
327  // If the extension is currently pending, return and we'll check again
328  // after the install is finished.
329  extensions::PendingExtensionManager* pending_manager =
330      extension_service->pending_extension_manager();
331  if (pending_manager->IsIdPending(extension->id()))
332    return false;
333
334  // If there is already a pending request from HotwordService, don't try
335  // to uninstall either.
336  if (reinstall_pending_)
337    return false;
338
339  // Check if the current locale matches the previous. If they don't match,
340  // uninstall the extension.
341  if (!ShouldReinstallHotwordExtension())
342    return false;
343
344  // Ensure the call to OnExtensionUninstalled was triggered by a language
345  // change so it's okay to reinstall.
346  reinstall_pending_ = true;
347
348  return UninstallHotwordExtension(extension_service);
349}
350
351bool HotwordService::UninstallHotwordExtension(
352    ExtensionService* extension_service) {
353  base::string16 error;
354  if (!extension_service->UninstallExtension(
355          extension_misc::kHotwordExtensionId,
356          extensions::UNINSTALL_REASON_INTERNAL_MANAGEMENT,
357          base::Bind(&base::DoNothing),
358          &error)) {
359    LOG(WARNING) << "Cannot uninstall extension with id "
360                 << extension_misc::kHotwordExtensionId
361                 << ": " << error;
362    reinstall_pending_ = false;
363    return false;
364  }
365  return true;
366}
367
368bool HotwordService::IsServiceAvailable() {
369  error_message_ = 0;
370
371  // Determine if the extension is available.
372  extensions::ExtensionSystem* system =
373      extensions::ExtensionSystem::Get(profile_);
374  ExtensionService* service = system->extension_service();
375  // Include disabled extensions (true parameter) since it may not be enabled
376  // if the user opted out.
377  const extensions::Extension* extension =
378      service->GetExtensionById(extension_misc::kHotwordExtensionId, true);
379  if (!extension)
380    error_message_ = IDS_HOTWORD_GENERIC_ERROR_MESSAGE;
381
382  RecordExtensionAvailabilityMetrics(service, extension);
383  RecordLoggingMetrics(profile_);
384
385  // Determine if NaCl is available.
386  bool nacl_enabled = false;
387  base::FilePath path;
388  if (PathService::Get(chrome::FILE_NACL_PLUGIN, &path)) {
389    content::WebPluginInfo info;
390    PluginPrefs* plugin_prefs = PluginPrefs::GetForProfile(profile_).get();
391    if (content::PluginService::GetInstance()->GetPluginInfoByPath(path, &info))
392      nacl_enabled = plugin_prefs->IsPluginEnabled(info);
393  }
394  if (!nacl_enabled)
395    error_message_ = IDS_HOTWORD_NACL_DISABLED_ERROR_MESSAGE;
396
397  RecordErrorMetrics(error_message_);
398
399  // Determine if the proper audio capabilities exist.
400  bool audio_capture_allowed =
401      profile_->GetPrefs()->GetBoolean(prefs::kAudioCaptureAllowed);
402  if (!audio_capture_allowed || !HotwordServiceFactory::IsMicrophoneAvailable())
403    error_message_ = IDS_HOTWORD_MICROPHONE_ERROR_MESSAGE;
404
405  return (error_message_ == 0) && IsHotwordAllowed();
406}
407
408bool HotwordService::IsHotwordAllowed() {
409  std::string group = base::FieldTrialList::FindFullName(
410      hotword_internal::kHotwordFieldTrialName);
411  return !group.empty() &&
412      group != hotword_internal::kHotwordFieldTrialDisabledGroupName &&
413      DoesHotwordSupportLanguage(profile_);
414}
415
416bool HotwordService::IsOptedIntoAudioLogging() {
417  // Do not opt the user in if the preference has not been set.
418  return
419      profile_->GetPrefs()->HasPrefPath(prefs::kHotwordAudioLoggingEnabled) &&
420      profile_->GetPrefs()->GetBoolean(prefs::kHotwordAudioLoggingEnabled);
421}
422
423void HotwordService::EnableHotwordExtension(
424    ExtensionService* extension_service) {
425  if (extension_service)
426    extension_service->EnableExtension(extension_misc::kHotwordExtensionId);
427}
428
429void HotwordService::DisableHotwordExtension(
430    ExtensionService* extension_service) {
431  if (extension_service) {
432    extension_service->DisableExtension(
433        extension_misc::kHotwordExtensionId,
434        extensions::Extension::DISABLE_USER_ACTION);
435  }
436}
437
438void HotwordService::OnHotwordSearchEnabledChanged(
439    const std::string& pref_name) {
440  DCHECK_EQ(pref_name, std::string(prefs::kHotwordSearchEnabled));
441
442  ExtensionService* extension_service = GetExtensionService(profile_);
443  if (profile_->GetPrefs()->GetBoolean(prefs::kHotwordSearchEnabled))
444    EnableHotwordExtension(extension_service);
445  else
446    DisableHotwordExtension(extension_service);
447}
448
449void HotwordService::RequestHotwordSession(HotwordClient* client) {
450  if (!IsServiceAvailable() || client_)
451    return;
452
453  client_ = client;
454
455  HotwordPrivateEventService* event_service =
456      BrowserContextKeyedAPIFactory<HotwordPrivateEventService>::Get(profile_);
457  if (event_service)
458    event_service->OnHotwordSessionRequested();
459}
460
461void HotwordService::StopHotwordSession(HotwordClient* client) {
462  if (!IsServiceAvailable())
463    return;
464
465  DCHECK(client_ == client);
466
467  client_ = NULL;
468  HotwordPrivateEventService* event_service =
469      BrowserContextKeyedAPIFactory<HotwordPrivateEventService>::Get(profile_);
470  if (event_service)
471    event_service->OnHotwordSessionStopped();
472}
473
474void HotwordService::SetPreviousLanguagePref() {
475  profile_->GetPrefs()->SetString(prefs::kHotwordPreviousLanguage,
476                                  GetCurrentLocale(profile_));
477}
478
479bool HotwordService::ShouldReinstallHotwordExtension() {
480  // If there is no previous locale pref, then this is the first install
481  // so no need to uninstall first.
482  if (!profile_->GetPrefs()->HasPrefPath(prefs::kHotwordPreviousLanguage))
483    return false;
484
485  std::string previous_locale =
486      profile_->GetPrefs()->GetString(prefs::kHotwordPreviousLanguage);
487  std::string locale = GetCurrentLocale(profile_);
488
489  // If it's a new locale, then the old extension should be uninstalled.
490  return locale != previous_locale &&
491      HotwordService::DoesHotwordSupportLanguage(profile_);
492}
493