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/extensions/pending_extension_manager.h"
16 #include "chrome/browser/extensions/updater/extension_updater.h"
17 #include "chrome/browser/extensions/webstore_startup_installer.h"
18 #include "chrome/browser/plugins/plugin_prefs.h"
19 #include "chrome/browser/profiles/profile.h"
20 #include "chrome/browser/search/hotword_service_factory.h"
21 #include "chrome/common/chrome_paths.h"
22 #include "chrome/common/extensions/extension_constants.h"
23 #include "chrome/common/pref_names.h"
24 #include "content/public/browser/browser_thread.h"
25 #include "content/public/browser/notification_service.h"
26 #include "content/public/browser/plugin_service.h"
27 #include "content/public/common/webplugininfo.h"
28 #include "extensions/browser/extension_system.h"
29 #include "extensions/common/extension.h"
30 #include "extensions/common/one_shot_event.h"
31 #include "grit/generated_resources.h"
32 #include "ui/base/l10n/l10n_util.h"
33
34 // The whole file relies on the extension systems but this file is built on
35 // some non-extension supported platforms and including an API header will cause
36 // a compile error since it depends on header files generated by .idl.
37 // TODO(mukai): clean up file dependencies and remove this clause.
38 #if defined(ENABLE_EXTENSIONS)
39 #include "chrome/browser/extensions/api/hotword_private/hotword_private_api.h"
40 #endif
41
42 #if defined(ENABLE_EXTENSIONS)
43 using extensions::BrowserContextKeyedAPIFactory;
44 using extensions::HotwordPrivateEventService;
45 #endif
46
47 namespace {
48
49 // Allowed languages for hotwording.
50 static const char* kSupportedLocales[] = {
51 "en",
52 "de",
53 "fr",
54 "ru"
55 };
56
57 // Enum describing the state of the hotword preference.
58 // This is used for UMA stats -- do not reorder or delete items; only add to
59 // the end.
60 enum HotwordEnabled {
61 UNSET = 0, // The hotword preference has not been set.
62 ENABLED, // The hotword preference is enabled.
63 DISABLED, // The hotword preference is disabled.
64 NUM_HOTWORD_ENABLED_METRICS
65 };
66
67 // Enum describing the availability state of the hotword extension.
68 // This is used for UMA stats -- do not reorder or delete items; only add to
69 // the end.
70 enum HotwordExtensionAvailability {
71 UNAVAILABLE = 0,
72 AVAILABLE,
73 PENDING_DOWNLOAD,
74 DISABLED_EXTENSION,
75 NUM_HOTWORD_EXTENSION_AVAILABILITY_METRICS
76 };
77
78 // Enum describing the types of errors that can arise when determining
79 // if hotwording can be used. NO_ERROR is used so it can be seen how often
80 // errors arise relative to when they do not.
81 // This is used for UMA stats -- do not reorder or delete items; only add to
82 // the end.
83 enum HotwordError {
84 NO_HOTWORD_ERROR = 0,
85 GENERIC_HOTWORD_ERROR,
86 NACL_HOTWORD_ERROR,
87 MICROPHONE_HOTWORD_ERROR,
88 NUM_HOTWORD_ERROR_METRICS
89 };
90
RecordExtensionAvailabilityMetrics(ExtensionService * service,const extensions::Extension * extension)91 void RecordExtensionAvailabilityMetrics(
92 ExtensionService* service,
93 const extensions::Extension* extension) {
94 HotwordExtensionAvailability availability_state = UNAVAILABLE;
95 if (extension) {
96 availability_state = AVAILABLE;
97 } else if (service->pending_extension_manager() &&
98 service->pending_extension_manager()->IsIdPending(
99 extension_misc::kHotwordExtensionId)) {
100 availability_state = PENDING_DOWNLOAD;
101 } else if (!service->IsExtensionEnabled(
102 extension_misc::kHotwordExtensionId)) {
103 availability_state = DISABLED_EXTENSION;
104 }
105 UMA_HISTOGRAM_ENUMERATION("Hotword.HotwordExtensionAvailability",
106 availability_state,
107 NUM_HOTWORD_EXTENSION_AVAILABILITY_METRICS);
108 }
109
RecordLoggingMetrics(Profile * profile)110 void RecordLoggingMetrics(Profile* profile) {
111 // If the user is not opted in to hotword voice search, the audio logging
112 // metric is not valid so it is not recorded.
113 if (!profile->GetPrefs()->GetBoolean(prefs::kHotwordSearchEnabled))
114 return;
115
116 UMA_HISTOGRAM_BOOLEAN(
117 "Hotword.HotwordAudioLogging",
118 profile->GetPrefs()->GetBoolean(prefs::kHotwordAudioLoggingEnabled));
119 }
120
RecordErrorMetrics(int error_message)121 void RecordErrorMetrics(int error_message) {
122 HotwordError error = NO_HOTWORD_ERROR;
123 switch (error_message) {
124 case IDS_HOTWORD_GENERIC_ERROR_MESSAGE:
125 error = GENERIC_HOTWORD_ERROR;
126 break;
127 case IDS_HOTWORD_NACL_DISABLED_ERROR_MESSAGE:
128 error = NACL_HOTWORD_ERROR;
129 break;
130 case IDS_HOTWORD_MICROPHONE_ERROR_MESSAGE:
131 error = MICROPHONE_HOTWORD_ERROR;
132 break;
133 default:
134 error = NO_HOTWORD_ERROR;
135 }
136
137 UMA_HISTOGRAM_ENUMERATION("Hotword.HotwordError",
138 error,
139 NUM_HOTWORD_ERROR_METRICS);
140 }
141
GetExtensionService(Profile * profile)142 ExtensionService* GetExtensionService(Profile* profile) {
143 CHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
144
145 extensions::ExtensionSystem* extension_system =
146 extensions::ExtensionSystem::Get(profile);
147 if (extension_system)
148 return extension_system->extension_service();
149 return NULL;
150 }
151
GetCurrentLocale(Profile * profile)152 std::string GetCurrentLocale(Profile* profile) {
153 std::string locale =
154 #if defined(OS_CHROMEOS)
155 // On ChromeOS locale is per-profile.
156 profile->GetPrefs()->GetString(prefs::kApplicationLocale);
157 #else
158 g_browser_process->GetApplicationLocale();
159 #endif
160 return locale;
161 }
162
163 } // namespace
164
165 namespace hotword_internal {
166 // Constants for the hotword field trial.
167 const char kHotwordFieldTrialName[] = "VoiceTrigger";
168 const char kHotwordFieldTrialDisabledGroupName[] = "Disabled";
169 // Old preference constant.
170 const char kHotwordUnusablePrefName[] = "hotword.search_enabled";
171 } // namespace hotword_internal
172
173 // static
DoesHotwordSupportLanguage(Profile * profile)174 bool HotwordService::DoesHotwordSupportLanguage(Profile* profile) {
175 std::string normalized_locale =
176 l10n_util::NormalizeLocale(GetCurrentLocale(profile));
177 StringToLowerASCII(&normalized_locale);
178
179 for (size_t i = 0; i < arraysize(kSupportedLocales); i++) {
180 if (normalized_locale.compare(0, 2, kSupportedLocales[i]) == 0)
181 return true;
182 }
183 return false;
184 }
185
HotwordService(Profile * profile)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_));
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
~HotwordService()236 HotwordService::~HotwordService() {
237 }
238
Observe(int type,const content::NotificationSource & source,const content::NotificationDetails & details)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();
255 }
256 }
257
OnExtensionUninstalled(content::BrowserContext * browser_context,const extensions::Extension * extension)258 void HotwordService::OnExtensionUninstalled(
259 content::BrowserContext* browser_context,
260 const extensions::Extension* extension) {
261 CHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
262
263 if (extension->id() != extension_misc::kHotwordExtensionId ||
264 profile_ != Profile::FromBrowserContext(browser_context) ||
265 !GetExtensionService(profile_))
266 return;
267
268 // If the extension wasn't uninstalled due to language change, don't try to
269 // reinstall it.
270 if (!reinstall_pending_)
271 return;
272
273 InstallHotwordExtensionFromWebstore();
274 SetPreviousLanguagePref();
275 }
276
InstallHotwordExtensionFromWebstore()277 void HotwordService::InstallHotwordExtensionFromWebstore() {
278 #if defined(ENABLE_EXTENSIONS)
279 installer_ = new extensions::WebstoreStartupInstaller(
280 extension_misc::kHotwordExtensionId,
281 profile_,
282 false,
283 extensions::WebstoreStandaloneInstaller::Callback());
284 installer_->BeginInstall();
285 #endif
286 }
287
OnExtensionInstalled(content::BrowserContext * browser_context,const extensions::Extension * extension)288 void HotwordService::OnExtensionInstalled(
289 content::BrowserContext* browser_context,
290 const extensions::Extension* extension) {
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
MaybeReinstallHotwordExtension()325 bool 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
UninstallHotwordExtension(ExtensionService * extension_service)361 bool HotwordService::UninstallHotwordExtension(
362 ExtensionService* extension_service) {
363 base::string16 error;
364 if (!extension_service->UninstallExtension(
365 extension_misc::kHotwordExtensionId, true, &error)) {
366 LOG(WARNING) << "Cannot uninstall extension with id "
367 << extension_misc::kHotwordExtensionId
368 << ": " << error;
369 reinstall_pending_ = false;
370 return false;
371 }
372 return true;
373 }
374
IsServiceAvailable()375 bool HotwordService::IsServiceAvailable() {
376 error_message_ = 0;
377
378 // Determine if the extension is available.
379 extensions::ExtensionSystem* system =
380 extensions::ExtensionSystem::Get(profile_);
381 ExtensionService* service = system->extension_service();
382 // Include disabled extensions (true parameter) since it may not be enabled
383 // if the user opted out.
384 const extensions::Extension* extension =
385 service->GetExtensionById(extension_misc::kHotwordExtensionId, true);
386 if (!extension)
387 error_message_ = IDS_HOTWORD_GENERIC_ERROR_MESSAGE;
388
389 RecordExtensionAvailabilityMetrics(service, extension);
390 RecordLoggingMetrics(profile_);
391
392 // NaCl and its associated functions are not available on most mobile
393 // platforms. ENABLE_EXTENSIONS covers those platforms and hey would not
394 // allow Hotwording anyways since it is an extension.
395 #if defined(ENABLE_EXTENSIONS)
396 // Determine if NaCl is available.
397 bool nacl_enabled = false;
398 base::FilePath path;
399 if (PathService::Get(chrome::FILE_NACL_PLUGIN, &path)) {
400 content::WebPluginInfo info;
401 PluginPrefs* plugin_prefs = PluginPrefs::GetForProfile(profile_).get();
402 if (content::PluginService::GetInstance()->GetPluginInfoByPath(path, &info))
403 nacl_enabled = plugin_prefs->IsPluginEnabled(info);
404 }
405 if (!nacl_enabled)
406 error_message_ = IDS_HOTWORD_NACL_DISABLED_ERROR_MESSAGE;
407 #endif
408
409 RecordErrorMetrics(error_message_);
410
411 // Determine if the proper audio capabilities exist.
412 bool audio_capture_allowed =
413 profile_->GetPrefs()->GetBoolean(prefs::kAudioCaptureAllowed);
414 if (!audio_capture_allowed || !HotwordServiceFactory::IsMicrophoneAvailable())
415 error_message_ = IDS_HOTWORD_MICROPHONE_ERROR_MESSAGE;
416
417 return (error_message_ == 0) && IsHotwordAllowed();
418 }
419
IsHotwordAllowed()420 bool HotwordService::IsHotwordAllowed() {
421 std::string group = base::FieldTrialList::FindFullName(
422 hotword_internal::kHotwordFieldTrialName);
423 return !group.empty() &&
424 group != hotword_internal::kHotwordFieldTrialDisabledGroupName &&
425 DoesHotwordSupportLanguage(profile_);
426 }
427
IsOptedIntoAudioLogging()428 bool HotwordService::IsOptedIntoAudioLogging() {
429 // Do not opt the user in if the preference has not been set.
430 return
431 profile_->GetPrefs()->HasPrefPath(prefs::kHotwordAudioLoggingEnabled) &&
432 profile_->GetPrefs()->GetBoolean(prefs::kHotwordAudioLoggingEnabled);
433 }
434
EnableHotwordExtension(ExtensionService * extension_service)435 void HotwordService::EnableHotwordExtension(
436 ExtensionService* extension_service) {
437 if (extension_service)
438 extension_service->EnableExtension(extension_misc::kHotwordExtensionId);
439 }
440
DisableHotwordExtension(ExtensionService * extension_service)441 void HotwordService::DisableHotwordExtension(
442 ExtensionService* extension_service) {
443 if (extension_service) {
444 extension_service->DisableExtension(
445 extension_misc::kHotwordExtensionId,
446 extensions::Extension::DISABLE_USER_ACTION);
447 }
448 }
449
OnHotwordSearchEnabledChanged(const std::string & pref_name)450 void HotwordService::OnHotwordSearchEnabledChanged(
451 const std::string& pref_name) {
452 DCHECK_EQ(pref_name, std::string(prefs::kHotwordSearchEnabled));
453
454 ExtensionService* extension_service = GetExtensionService(profile_);
455 if (profile_->GetPrefs()->GetBoolean(prefs::kHotwordSearchEnabled))
456 EnableHotwordExtension(extension_service);
457 else
458 DisableHotwordExtension(extension_service);
459 }
460
RequestHotwordSession(HotwordClient * client)461 void HotwordService::RequestHotwordSession(HotwordClient* client) {
462 #if defined(ENABLE_EXTENSIONS)
463 if (!IsServiceAvailable() || client_)
464 return;
465
466 client_ = client;
467
468 HotwordPrivateEventService* event_service =
469 BrowserContextKeyedAPIFactory<HotwordPrivateEventService>::Get(profile_);
470 if (event_service)
471 event_service->OnHotwordSessionRequested();
472 #endif
473 }
474
StopHotwordSession(HotwordClient * client)475 void HotwordService::StopHotwordSession(HotwordClient* client) {
476 #if defined(ENABLE_EXTENSIONS)
477 if (!IsServiceAvailable())
478 return;
479
480 DCHECK(client_ == client);
481
482 client_ = NULL;
483 HotwordPrivateEventService* event_service =
484 BrowserContextKeyedAPIFactory<HotwordPrivateEventService>::Get(profile_);
485 if (event_service)
486 event_service->OnHotwordSessionStopped();
487 #endif
488 }
489
SetPreviousLanguagePref()490 void HotwordService::SetPreviousLanguagePref() {
491 profile_->GetPrefs()->SetString(prefs::kHotwordPreviousLanguage,
492 GetCurrentLocale(profile_));
493 }
494
ShouldReinstallHotwordExtension()495 bool HotwordService::ShouldReinstallHotwordExtension() {
496 // If there is no previous locale pref, then this is the first install
497 // so no need to uninstall first.
498 if (!profile_->GetPrefs()->HasPrefPath(prefs::kHotwordPreviousLanguage))
499 return false;
500
501 std::string previous_locale =
502 profile_->GetPrefs()->GetString(prefs::kHotwordPreviousLanguage);
503 std::string locale = GetCurrentLocale(profile_);
504
505 // If it's a new locale, then the old extension should be uninstalled.
506 return locale != previous_locale &&
507 HotwordService::DoesHotwordSupportLanguage(profile_);
508 }
509