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