Home | History | Annotate | Download | only in search
      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 
     39 using extensions::BrowserContextKeyedAPIFactory;
     40 using extensions::HotwordPrivateEventService;
     41 
     42 namespace {
     43 
     44 // Allowed languages for hotwording.
     45 static 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.
     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
     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.
     65 enum 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.
     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
     84 };
     85 
     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;
     99   }
    100   UMA_HISTOGRAM_ENUMERATION("Hotword.HotwordExtensionAvailability",
    101                             availability_state,
    102                             NUM_HOTWORD_EXTENSION_AVAILABILITY_METRICS);
    103 }
    104 
    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;
    110 
    111   UMA_HISTOGRAM_BOOLEAN(
    112       "Hotword.HotwordAudioLogging",
    113       profile->GetPrefs()->GetBoolean(prefs::kHotwordAudioLoggingEnabled));
    114 }
    115 
    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;
    130   }
    131 
    132   UMA_HISTOGRAM_ENUMERATION("Hotword.HotwordError",
    133                             error,
    134                             NUM_HOTWORD_ERROR_METRICS);
    135 }
    136 
    137 ExtensionService* 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 
    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;
    152   }
    153 #endif
    154   return g_browser_process->GetApplicationLocale();
    155 }
    156 
    157 }  // namespace
    158 
    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
    166 
    167 // static
    168 bool 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
    181 bool HotwordService::IsExperimentalHotwordingEnabled() {
    182   CommandLine* command_line = CommandLine::ForCurrentProcess();
    183   return command_line->HasSwitch(switches::kEnableExperimentalHotwording);
    184 }
    185 
    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 
    236 HotwordService::~HotwordService() {
    237 }
    238 
    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 
    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));
    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 
    278 void HotwordService::InstallHotwordExtensionFromWebstore() {
    279   installer_ = new extensions::WebstoreStartupInstaller(
    280       extension_misc::kHotwordExtensionId,
    281       profile_,
    282       false,
    283       extensions::WebstoreStandaloneInstaller::Callback());
    284   installer_->BeginInstall();
    285 }
    286 
    287 void 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 
    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 
    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;
    374   }
    375   return true;
    376 }
    377 
    378 bool 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 
    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_);
    432 }
    433 
    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);
    439 }
    440 
    441 void HotwordService::EnableHotwordExtension(
    442     ExtensionService* extension_service) {
    443   if (extension_service)
    444     extension_service->EnableExtension(extension_misc::kHotwordExtensionId);
    445 }
    446 
    447 void 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 
    456 void 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 
    472 HotwordService::LaunchMode
    473 HotwordService::GetHotwordAudioVerificationLaunchMode() {
    474   return hotword_audio_verification_launch_mode_;
    475 }
    476 
    477 void 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 
    488 void 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 
    500 void 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 
    513 void HotwordService::SetPreviousLanguagePref() {
    514   profile_->GetPrefs()->SetString(prefs::kHotwordPreviousLanguage,
    515                                   GetCurrentLocale(profile_));
    516 }
    517 
    518 bool 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