Adding instrumentation to locate the source of jankiness
[chromium-blink-merge.git] / chrome / browser / search / hotword_service.cc
blobc8bf7052843e9341eb97979ae99b4535468377eb
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.
5 #include "chrome/browser/search/hotword_service.h"
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"
39 using extensions::BrowserContextKeyedAPIFactory;
40 using extensions::HotwordPrivateEventService;
42 namespace {
44 // Allowed languages for hotwording.
45 static const char* kSupportedLocales[] = {
46 "en",
47 "de",
48 "fr",
49 "ru"
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.
55 enum 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
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.
65 enum HotwordExtensionAvailability {
66 UNAVAILABLE = 0,
67 AVAILABLE,
68 PENDING_DOWNLOAD,
69 DISABLED_EXTENSION,
70 NUM_HOTWORD_EXTENSION_AVAILABILITY_METRICS
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.
78 enum HotwordError {
79 NO_HOTWORD_ERROR = 0,
80 GENERIC_HOTWORD_ERROR,
81 NACL_HOTWORD_ERROR,
82 MICROPHONE_HOTWORD_ERROR,
83 NUM_HOTWORD_ERROR_METRICS
86 void 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;
100 UMA_HISTOGRAM_ENUMERATION("Hotword.HotwordExtensionAvailability",
101 availability_state,
102 NUM_HOTWORD_EXTENSION_AVAILABILITY_METRICS);
105 void 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;
111 UMA_HISTOGRAM_BOOLEAN(
112 "Hotword.HotwordAudioLogging",
113 profile->GetPrefs()->GetBoolean(prefs::kHotwordAudioLoggingEnabled));
116 void 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;
132 UMA_HISTOGRAM_ENUMERATION("Hotword.HotwordError",
133 error,
134 NUM_HOTWORD_ERROR_METRICS);
137 ExtensionService* GetExtensionService(Profile* profile) {
138 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
140 extensions::ExtensionSystem* extension_system =
141 extensions::ExtensionSystem::Get(profile);
142 return extension_system ? extension_system->extension_service() : NULL;
145 std::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;
153 #endif
154 return g_browser_process->GetApplicationLocale();
157 } // namespace
159 namespace hotword_internal {
160 // Constants for the hotword field trial.
161 const char kHotwordFieldTrialName[] = "VoiceTrigger";
162 const char kHotwordFieldTrialDisabledGroupName[] = "Disabled";
163 // Old preference constant.
164 const char kHotwordUnusablePrefName[] = "hotword.search_enabled";
165 } // namespace hotword_internal
167 // static
168 bool HotwordService::DoesHotwordSupportLanguage(Profile* profile) {
169 std::string normalized_locale =
170 l10n_util::NormalizeLocale(GetCurrentLocale(profile));
171 base::StringToLowerASCII(&normalized_locale);
173 for (size_t i = 0; i < arraysize(kSupportedLocales); i++) {
174 if (normalized_locale.compare(0, 2, kSupportedLocales[i]) == 0)
175 return true;
177 return false;
180 // static
181 bool HotwordService::IsExperimentalHotwordingEnabled() {
182 CommandLine* command_line = CommandLine::ForCurrentProcess();
183 return command_line->HasSwitch(switches::kEnableExperimentalHotwording);
186 HotwordService::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_));
209 UMA_HISTOGRAM_ENUMERATION("Hotword.Enabled", enabled_state,
210 NUM_HOTWORD_ENABLED_METRICS);
212 pref_registrar_.Init(profile_->GetPrefs());
213 pref_registrar_.Add(
214 prefs::kHotwordSearchEnabled,
215 base::Bind(&HotwordService::OnHotwordSearchEnabledChanged,
216 base::Unretained(this)));
218 registrar_.Add(this,
219 chrome::NOTIFICATION_BROWSER_WINDOW_READY,
220 content::NotificationService::AllSources());
222 extensions::ExtensionSystem::Get(profile_)->ready().Post(
223 FROM_HERE,
224 base::Bind(base::IgnoreResult(
225 &HotwordService::MaybeReinstallHotwordExtension),
226 weak_factory_.GetWeakPtr()));
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);
236 HotwordService::~HotwordService() {
239 void 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();
258 void HotwordService::OnExtensionUninstalled(
259 content::BrowserContext* browser_context,
260 const extensions::Extension* extension,
261 extensions::UninstallReason reason) {
262 CHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
264 if (extension->id() != extension_misc::kHotwordExtensionId ||
265 profile_ != Profile::FromBrowserContext(browser_context) ||
266 !GetExtensionService(profile_))
267 return;
269 // If the extension wasn't uninstalled due to language change, don't try to
270 // reinstall it.
271 if (!reinstall_pending_)
272 return;
274 InstallHotwordExtensionFromWebstore();
275 SetPreviousLanguagePref();
278 void HotwordService::InstallHotwordExtensionFromWebstore() {
279 installer_ = new extensions::WebstoreStartupInstaller(
280 extension_misc::kHotwordExtensionId,
281 profile_,
282 false,
283 extensions::WebstoreStandaloneInstaller::Callback());
284 installer_->BeginInstall();
287 void HotwordService::OnExtensionInstalled(
288 content::BrowserContext* browser_context,
289 const extensions::Extension* extension,
290 bool is_update) {
292 if (extension->id() != extension_misc::kHotwordExtensionId ||
293 profile_ != Profile::FromBrowserContext(browser_context))
294 return;
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();
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;
311 // Now that the extension is installed, if the user has not selected
312 // the preference on, make sure it is turned off.
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_));
325 bool HotwordService::MaybeReinstallHotwordExtension() {
326 CHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
328 ExtensionService* extension_service = GetExtensionService(profile_);
329 if (!extension_service)
330 return false;
332 const extensions::Extension* extension = extension_service->GetExtensionById(
333 extension_misc::kHotwordExtensionId, true);
334 if (!extension)
335 return false;
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;
344 // If there is already a pending request from HotwordService, don't try
345 // to uninstall either.
346 if (reinstall_pending_)
347 return false;
349 // Check if the current locale matches the previous. If they don't match,
350 // uninstall the extension.
351 if (!ShouldReinstallHotwordExtension())
352 return false;
354 // Ensure the call to OnExtensionUninstalled was triggered by a language
355 // change so it's okay to reinstall.
356 reinstall_pending_ = true;
358 return UninstallHotwordExtension(extension_service);
361 bool 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;
375 return true;
378 bool HotwordService::IsServiceAvailable() {
379 error_message_ = 0;
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;
395 const extensions::Extension* extension =
396 service->GetExtensionById(extensionId, true);
397 if (!extension)
398 error_message_ = IDS_HOTWORD_GENERIC_ERROR_MESSAGE;
400 RecordExtensionAvailabilityMetrics(service, extension);
401 RecordLoggingMetrics(profile_);
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);
412 if (!nacl_enabled)
413 error_message_ = IDS_HOTWORD_NACL_DISABLED_ERROR_MESSAGE;
415 RecordErrorMetrics(error_message_);
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;
423 return (error_message_ == 0) && IsHotwordAllowed();
426 bool 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_);
434 bool 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);
441 bool HotwordService::IsAlwaysOnEnabled() {
442 return
443 profile_->GetPrefs()->HasPrefPath(prefs::kHotwordAlwaysOnSearchEnabled) &&
444 profile_->GetPrefs()->GetBoolean(prefs::kHotwordAlwaysOnSearchEnabled);
447 void HotwordService::EnableHotwordExtension(
448 ExtensionService* extension_service) {
449 if (extension_service)
450 extension_service->EnableExtension(extension_misc::kHotwordExtensionId);
453 void HotwordService::DisableHotwordExtension(
454 ExtensionService* extension_service) {
455 if (extension_service) {
456 extension_service->DisableExtension(
457 extension_misc::kHotwordExtensionId,
458 extensions::Extension::DISABLE_USER_ACTION);
462 void HotwordService::LaunchHotwordAudioVerificationApp(
463 const LaunchMode& launch_mode) {
464 hotword_audio_verification_launch_mode_ = launch_mode;
466 ExtensionService* extension_service = GetExtensionService(profile_);
467 if (!extension_service)
468 return;
469 const extensions::Extension* extension = extension_service->GetExtensionById(
470 extension_misc::kHotwordAudioVerificationAppId, true);
471 if (!extension)
472 return;
474 OpenApplication(AppLaunchParams(
475 profile_, extension, extensions::LAUNCH_CONTAINER_WINDOW, NEW_WINDOW));
478 HotwordService::LaunchMode
479 HotwordService::GetHotwordAudioVerificationLaunchMode() {
480 return hotword_audio_verification_launch_mode_;
483 void HotwordService::OnHotwordSearchEnabledChanged(
484 const std::string& pref_name) {
485 DCHECK_EQ(pref_name, std::string(prefs::kHotwordSearchEnabled));
487 ExtensionService* extension_service = GetExtensionService(profile_);
488 if (profile_->GetPrefs()->GetBoolean(prefs::kHotwordSearchEnabled))
489 EnableHotwordExtension(extension_service);
490 else
491 DisableHotwordExtension(extension_service);
494 void HotwordService::RequestHotwordSession(HotwordClient* client) {
495 if (!IsServiceAvailable() || (client_ && client_ != client))
496 return;
498 client_ = client;
500 HotwordPrivateEventService* event_service =
501 BrowserContextKeyedAPIFactory<HotwordPrivateEventService>::Get(profile_);
502 if (event_service)
503 event_service->OnHotwordSessionRequested();
506 void HotwordService::StopHotwordSession(HotwordClient* client) {
507 if (!IsServiceAvailable())
508 return;
510 // Do nothing if there's no client.
511 if (!client_)
512 return;
513 DCHECK(client_ == client);
515 client_ = NULL;
516 HotwordPrivateEventService* event_service =
517 BrowserContextKeyedAPIFactory<HotwordPrivateEventService>::Get(profile_);
518 if (event_service)
519 event_service->OnHotwordSessionStopped();
522 void HotwordService::SetPreviousLanguagePref() {
523 profile_->GetPrefs()->SetString(prefs::kHotwordPreviousLanguage,
524 GetCurrentLocale(profile_));
527 bool HotwordService::ShouldReinstallHotwordExtension() {
528 // If there is no previous locale pref, then this is the first install
529 // so no need to uninstall first.
530 if (!profile_->GetPrefs()->HasPrefPath(prefs::kHotwordPreviousLanguage))
531 return false;
533 std::string previous_locale =
534 profile_->GetPrefs()->GetString(prefs::kHotwordPreviousLanguage);
535 std::string locale = GetCurrentLocale(profile_);
537 // If it's a new locale, then the old extension should be uninstalled.
538 return locale != previous_locale &&
539 HotwordService::DoesHotwordSupportLanguage(profile_);