| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chromeos/services/assistant/service.h" |
| |
| #include <algorithm> |
| #include <utility> |
| |
| #include "ash/public/cpp/session/session_controller.h" |
| #include "ash/public/interfaces/constants.mojom.h" |
| #include "base/bind.h" |
| #include "base/command_line.h" |
| #include "base/logging.h" |
| #include "base/rand_util.h" |
| #include "base/single_thread_task_runner.h" |
| #include "base/timer/timer.h" |
| #include "build/buildflag.h" |
| #include "chromeos/assistant/buildflags.h" |
| #include "chromeos/audio/cras_audio_handler.h" |
| #include "chromeos/constants/chromeos_switches.h" |
| #include "chromeos/dbus/dbus_thread_manager.h" |
| #include "chromeos/dbus/power_manager/power_supply_properties.pb.h" |
| #include "chromeos/services/assistant/assistant_manager_service.h" |
| #include "chromeos/services/assistant/assistant_settings_manager.h" |
| #include "chromeos/services/assistant/fake_assistant_manager_service_impl.h" |
| #include "chromeos/services/assistant/fake_assistant_settings_manager_impl.h" |
| #include "chromeos/services/assistant/public/features.h" |
| #include "google_apis/gaia/google_service_auth_error.h" |
| #include "services/identity/public/cpp/scope_set.h" |
| #include "services/identity/public/mojom/constants.mojom.h" |
| #include "services/network/public/cpp/shared_url_loader_factory.h" |
| #include "services/service_manager/public/cpp/connector.h" |
| |
| #if BUILDFLAG(ENABLE_CROS_LIBASSISTANT) |
| #include "chromeos/assistant/internal/internal_constants.h" |
| #include "chromeos/services/assistant/assistant_manager_service_impl.h" |
| #include "chromeos/services/assistant/assistant_settings_manager_impl.h" |
| #include "chromeos/services/assistant/utils.h" |
| #include "services/device/public/mojom/battery_monitor.mojom.h" |
| #include "services/device/public/mojom/constants.mojom.h" |
| #endif |
| |
| namespace chromeos { |
| namespace assistant { |
| |
| namespace { |
| |
| constexpr char kScopeAuthGcm[] = "https://www.googleapis.com/auth/gcm"; |
| constexpr char kScopeAssistant[] = |
| "https://www.googleapis.com/auth/assistant-sdk-prototype"; |
| constexpr char kScopeClearCutLog[] = "https://www.googleapis.com/auth/cclog"; |
| |
| constexpr base::TimeDelta kMinTokenRefreshDelay = |
| base::TimeDelta::FromMilliseconds(1000); |
| constexpr base::TimeDelta kMaxTokenRefreshDelay = |
| base::TimeDelta::FromMilliseconds(60 * 1000); |
| |
| } // namespace |
| |
| Service::Service(service_manager::mojom::ServiceRequest request, |
| network::NetworkConnectionTracker* network_connection_tracker, |
| std::unique_ptr<network::SharedURLLoaderFactoryInfo> |
| url_loader_factory_info) |
| : service_binding_(this, std::move(request)), |
| platform_binding_(this), |
| token_refresh_timer_(std::make_unique<base::OneShotTimer>()), |
| main_task_runner_(base::SequencedTaskRunnerHandle::Get()), |
| power_manager_observer_(this), |
| network_connection_tracker_(network_connection_tracker), |
| url_loader_factory_info_(std::move(url_loader_factory_info)), |
| weak_ptr_factory_(this) { |
| registry_.AddInterface<mojom::AssistantPlatform>(base::BindRepeating( |
| &Service::BindAssistantPlatformConnection, base::Unretained(this))); |
| |
| // TODO(xiaohuic): in MASH we will need to setup the dbus client if assistant |
| // service runs in its own process. |
| chromeos::PowerManagerClient* power_manager_client = |
| chromeos::PowerManagerClient::Get(); |
| power_manager_observer_.Add(power_manager_client); |
| power_manager_client->RequestStatusUpdate(); |
| } |
| |
| Service::~Service() { |
| auto* const session_controller = ash::SessionController::Get(); |
| if (observing_ash_session_ && session_controller) { |
| session_controller->RemoveSessionActivationObserverForAccountId(account_id_, |
| this); |
| } |
| } |
| |
| void Service::RequestAccessToken() { |
| // Bypass access token fetching under signed out mode. |
| if (is_signed_out_mode_) |
| return; |
| |
| VLOG(1) << "Start requesting access token."; |
| GetIdentityAccessor()->GetPrimaryAccountInfo(base::BindOnce( |
| &Service::GetPrimaryAccountInfoCallback, base::Unretained(this))); |
| } |
| |
| bool Service::ShouldEnableHotword() { |
| bool dsp_available = false; |
| chromeos::AudioDeviceList devices; |
| chromeos::CrasAudioHandler::Get()->GetAudioDevices(&devices); |
| for (const chromeos::AudioDevice& device : devices) { |
| if (device.type == chromeos::AUDIO_TYPE_HOTWORD) { |
| dsp_available = true; |
| } |
| } |
| |
| // Disable hotword if hotword is not set to always on and power source is not |
| // connected. |
| if (!dsp_available && !assistant_state_.hotword_always_on().value() && |
| !power_source_connected_) { |
| return false; |
| } |
| |
| return assistant_state_.hotword_enabled().value(); |
| } |
| |
| void Service::SetIdentityAccessorForTesting( |
| identity::mojom::IdentityAccessorPtr identity_accessor) { |
| identity_accessor_ = std::move(identity_accessor); |
| } |
| |
| void Service::SetAssistantManagerForTesting( |
| std::unique_ptr<AssistantManagerService> assistant_manager_service) { |
| assistant_manager_service_ = std::move(assistant_manager_service); |
| } |
| |
| void Service::SetTimerForTesting(std::unique_ptr<base::OneShotTimer> timer) { |
| token_refresh_timer_ = std::move(timer); |
| } |
| |
| void Service::OnStart() {} |
| |
| void Service::OnBindInterface( |
| const service_manager::BindSourceInfo& source_info, |
| const std::string& interface_name, |
| mojo::ScopedMessagePipeHandle interface_pipe) { |
| registry_.BindInterface(interface_name, std::move(interface_pipe)); |
| } |
| |
| void Service::BindAssistantConnection(mojom::AssistantRequest request) { |
| DCHECK(assistant_manager_service_); |
| bindings_.AddBinding(assistant_manager_service_.get(), std::move(request)); |
| } |
| |
| void Service::BindAssistantPlatformConnection( |
| mojom::AssistantPlatformRequest request) { |
| platform_binding_.Bind(std::move(request)); |
| } |
| |
| void Service::PowerChanged(const power_manager::PowerSupplyProperties& prop) { |
| const bool power_source_connected = |
| prop.external_power() == power_manager::PowerSupplyProperties::AC; |
| if (power_source_connected == power_source_connected_) |
| return; |
| |
| power_source_connected_ = power_source_connected; |
| UpdateAssistantManagerState(); |
| } |
| |
| void Service::SuspendDone(const base::TimeDelta& sleep_duration) { |
| // |token_refresh_timer_| may become stale during sleeping, so we immediately |
| // request a new token to make sure it is fresh. |
| if (token_refresh_timer_->IsRunning()) { |
| token_refresh_timer_->AbandonAndStop(); |
| RequestAccessToken(); |
| } |
| } |
| |
| void Service::OnSessionActivated(bool activated) { |
| DCHECK(client_); |
| session_active_ = activated; |
| |
| if (assistant_manager_service_->GetState() != |
| AssistantManagerService::State::RUNNING) { |
| return; |
| } |
| |
| client_->OnAssistantStatusChanged(activated /* running */); |
| UpdateListeningState(); |
| } |
| |
| void Service::OnLockStateChanged(bool locked) { |
| locked_ = locked; |
| UpdateListeningState(); |
| } |
| |
| void Service::OnVoiceInteractionSettingsEnabled(bool enabled) { |
| UpdateAssistantManagerState(); |
| } |
| |
| void Service::OnVoiceInteractionHotwordEnabled(bool enabled) { |
| UpdateAssistantManagerState(); |
| } |
| |
| void Service::OnLocaleChanged(const std::string& locale) { |
| UpdateAssistantManagerState(); |
| } |
| |
| void Service::OnArcPlayStoreEnabledChanged(bool enabled) { |
| UpdateAssistantManagerState(); |
| } |
| |
| void Service::OnLockedFullScreenStateChanged(bool enabled) { |
| UpdateListeningState(); |
| } |
| |
| void Service::OnVoiceInteractionHotwordAlwaysOn(bool always_on) { |
| // No need to update hotword status if power source is connected. |
| if (power_source_connected_) |
| return; |
| |
| UpdateAssistantManagerState(); |
| } |
| |
| void Service::UpdateAssistantManagerState() { |
| if (!assistant_state_.hotword_enabled().has_value() || |
| !assistant_state_.settings_enabled().has_value() || |
| !assistant_state_.hotword_always_on().has_value() || |
| !assistant_state_.locale().has_value() || |
| (!access_token_.has_value() && !is_signed_out_mode_) || |
| !assistant_state_.arc_play_store_enabled().has_value()) { |
| // Assistant state has not finished initialization, let's wait. |
| return; |
| } |
| |
| if (!assistant_manager_service_) |
| CreateAssistantManagerService(); |
| |
| switch (assistant_manager_service_->GetState()) { |
| case AssistantManagerService::State::STOPPED: |
| if (assistant_state_.settings_enabled().value()) { |
| assistant_manager_service_->Start( |
| is_signed_out_mode_ ? base::nullopt : access_token_, |
| ShouldEnableHotword(), |
| base::BindOnce( |
| [](scoped_refptr<base::SequencedTaskRunner> task_runner, |
| base::OnceCallback<void()> callback) { |
| task_runner->PostTask(FROM_HERE, std::move(callback)); |
| }, |
| main_task_runner_, |
| base::BindOnce(&Service::FinalizeAssistantManagerService, |
| weak_ptr_factory_.GetWeakPtr()))); |
| DVLOG(1) << "Request Assistant start"; |
| } |
| break; |
| case AssistantManagerService::State::RUNNING: |
| case AssistantManagerService::State::STARTED: |
| if (assistant_state_.settings_enabled().value()) { |
| if (!is_signed_out_mode_) |
| assistant_manager_service_->SetAccessToken(access_token_.value()); |
| assistant_manager_service_->EnableHotword(ShouldEnableHotword()); |
| assistant_manager_service_->SetArcPlayStoreEnabled( |
| assistant_state_.arc_play_store_enabled().value()); |
| } else { |
| StopAssistantManagerService(); |
| } |
| break; |
| } |
| } |
| |
| void Service::BindAssistantSettingsManager( |
| mojom::AssistantSettingsManagerRequest request) { |
| DCHECK(assistant_manager_service_); |
| assistant_manager_service_->GetAssistantSettingsManager()->BindRequest( |
| std::move(request)); |
| } |
| |
| void Service::Init(mojom::ClientPtr client, |
| mojom::DeviceActionsPtr device_actions, |
| bool is_test) { |
| is_test_ = is_test; |
| client_ = std::move(client); |
| device_actions_ = std::move(device_actions); |
| assistant_state_.Init(service_binding_.GetConnector()); |
| assistant_state_.AddObserver(this); |
| |
| // Don't fetch token for test. |
| if (base::CommandLine::ForCurrentProcess()->HasSwitch( |
| chromeos::switches::kDisableGaiaServices)) { |
| is_signed_out_mode_ = true; |
| return; |
| } |
| |
| RequestAccessToken(); |
| } |
| |
| identity::mojom::IdentityAccessor* Service::GetIdentityAccessor() { |
| if (!identity_accessor_) { |
| service_binding_.GetConnector()->BindInterface( |
| identity::mojom::kServiceName, mojo::MakeRequest(&identity_accessor_)); |
| } |
| return identity_accessor_.get(); |
| } |
| |
| void Service::GetPrimaryAccountInfoCallback( |
| const base::Optional<CoreAccountInfo>& account_info, |
| const identity::AccountState& account_state) { |
| if (!account_info.has_value() || !account_state.has_refresh_token || |
| account_info.value().gaia.empty()) { |
| LOG(ERROR) << "Failed to retrieve primary account info."; |
| RetryRefreshToken(); |
| return; |
| } |
| account_id_ = AccountIdFromAccountInfo(account_info.value()); |
| identity::ScopeSet scopes; |
| scopes.insert(kScopeAssistant); |
| scopes.insert(kScopeAuthGcm); |
| if (features::IsClearCutLogEnabled()) |
| scopes.insert(kScopeClearCutLog); |
| identity_accessor_->GetAccessToken( |
| account_info.value().account_id, scopes, "cros_assistant", |
| base::BindOnce(&Service::GetAccessTokenCallback, base::Unretained(this))); |
| } |
| |
| void Service::GetAccessTokenCallback(const base::Optional<std::string>& token, |
| base::Time expiration_time, |
| const GoogleServiceAuthError& error) { |
| if (!token.has_value()) { |
| LOG(ERROR) << "Failed to retrieve token, error: " << error.ToString(); |
| RetryRefreshToken(); |
| return; |
| } |
| |
| access_token_ = token; |
| UpdateAssistantManagerState(); |
| token_refresh_timer_->Start(FROM_HERE, expiration_time - base::Time::Now(), |
| this, &Service::RequestAccessToken); |
| } |
| |
| void Service::RetryRefreshToken() { |
| base::TimeDelta backoff_delay = |
| std::min(kMinTokenRefreshDelay * |
| (1 << (token_refresh_error_backoff_factor - 1)), |
| kMaxTokenRefreshDelay) + |
| base::RandDouble() * kMinTokenRefreshDelay; |
| if (backoff_delay < kMaxTokenRefreshDelay) |
| ++token_refresh_error_backoff_factor; |
| token_refresh_timer_->Start(FROM_HERE, backoff_delay, this, |
| &Service::RequestAccessToken); |
| } |
| |
| void Service::CreateAssistantManagerService() { |
| #if BUILDFLAG(ENABLE_CROS_LIBASSISTANT) |
| if (is_test_) { |
| // Use fake service in browser tests. |
| assistant_manager_service_ = |
| std::make_unique<FakeAssistantManagerServiceImpl>(); |
| return; |
| } |
| |
| device::mojom::BatteryMonitorPtr battery_monitor; |
| service_binding_.GetConnector()->BindInterface( |
| device::mojom::kServiceName, mojo::MakeRequest(&battery_monitor)); |
| |
| // |assistant_manager_service_| is only created once. |
| DCHECK(url_loader_factory_info_); |
| assistant_manager_service_ = std::make_unique<AssistantManagerServiceImpl>( |
| service_binding_.GetConnector(), std::move(battery_monitor), this, |
| network_connection_tracker_, std::move(url_loader_factory_info_)); |
| #else |
| assistant_manager_service_ = |
| std::make_unique<FakeAssistantManagerServiceImpl>(); |
| #endif |
| } |
| |
| void Service::FinalizeAssistantManagerService() { |
| DCHECK(assistant_manager_service_->GetState() == |
| AssistantManagerService::State::RUNNING); |
| |
| // Using session_observer_binding_ as a flag to control onetime initialization |
| if (!observing_ash_session_) { |
| // Bind to the AssistantController in ash. |
| service_binding_.GetConnector()->BindInterface(ash::mojom::kServiceName, |
| &assistant_controller_); |
| |
| mojom::AssistantPtr ptr; |
| BindAssistantConnection(mojo::MakeRequest(&ptr)); |
| assistant_controller_->SetAssistant(std::move(ptr)); |
| |
| if (features::IsTimerNotificationEnabled()) { |
| // Bind to the AssistantAlarmTimerController in ash. |
| service_binding_.GetConnector()->BindInterface( |
| ash::mojom::kServiceName, &assistant_alarm_timer_controller_); |
| } |
| |
| // Bind to the AssistantNotificationController in ash. |
| service_binding_.GetConnector()->BindInterface( |
| ash::mojom::kServiceName, &assistant_notification_controller_); |
| |
| // Bind to the AssistantScreenContextController in ash. |
| service_binding_.GetConnector()->BindInterface( |
| ash::mojom::kServiceName, &assistant_screen_context_controller_); |
| |
| registry_.AddInterface<mojom::Assistant>(base::BindRepeating( |
| &Service::BindAssistantConnection, base::Unretained(this))); |
| |
| registry_.AddInterface<mojom::AssistantSettingsManager>(base::BindRepeating( |
| &Service::BindAssistantSettingsManager, base::Unretained(this))); |
| |
| AddAshSessionObserver(); |
| } |
| |
| client_->OnAssistantStatusChanged(true /* running */); |
| UpdateListeningState(); |
| DVLOG(1) << "Assistant is running"; |
| } |
| |
| void Service::StopAssistantManagerService() { |
| assistant_manager_service_->Stop(); |
| weak_ptr_factory_.InvalidateWeakPtrs(); |
| client_->OnAssistantStatusChanged(false /* running */); |
| } |
| |
| void Service::AddAshSessionObserver() { |
| observing_ash_session_ = true; |
| ash::SessionController::Get()->AddSessionActivationObserverForAccountId( |
| account_id_, this); |
| } |
| |
| void Service::UpdateListeningState() { |
| if (assistant_manager_service_->GetState() != |
| AssistantManagerService::State::RUNNING) { |
| return; |
| } |
| |
| bool should_listen = |
| !locked_ && |
| !assistant_state_.locked_full_screen_enabled().value_or(false) && |
| session_active_; |
| DVLOG(1) << "Update assistant listening state: " << should_listen; |
| assistant_manager_service_->EnableListening(should_listen); |
| } |
| |
| } // namespace assistant |
| } // namespace chromeos |