| // 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/assistant_manager_service_impl.h" |
| |
| #include <algorithm> |
| #include <memory> |
| #include <utility> |
| |
| #include "ash/public/cpp/assistant/assistant_state_base.h" |
| #include "base/barrier_closure.h" |
| #include "base/bind.h" |
| #include "base/feature_list.h" |
| #include "base/i18n/rtl.h" |
| #include "base/logging.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/run_loop.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task/post_task.h" |
| #include "base/unguessable_token.h" |
| #include "chromeos/assistant/internal/internal_constants.h" |
| #include "chromeos/assistant/internal/internal_util.h" |
| #include "chromeos/assistant/internal/proto/google3/assistant/api/client_input/warmer_welcome_input.pb.h" |
| #include "chromeos/assistant/internal/proto/google3/assistant/api/client_op/device_args.pb.h" |
| #include "chromeos/dbus/util/version_loader.h" |
| #include "chromeos/services/assistant/assistant_communication_error_observer.h" |
| #include "chromeos/services/assistant/assistant_manager_service_delegate.h" |
| #include "chromeos/services/assistant/constants.h" |
| #include "chromeos/services/assistant/media_session/assistant_media_session.h" |
| #include "chromeos/services/assistant/platform_api_impl.h" |
| #include "chromeos/services/assistant/public/features.h" |
| #include "chromeos/services/assistant/service_context.h" |
| #include "chromeos/services/assistant/utils.h" |
| #include "chromeos/strings/grit/chromeos_strings.h" |
| #include "libassistant/shared/internal_api/alarm_timer_manager.h" |
| #include "libassistant/shared/internal_api/alarm_timer_types.h" |
| #include "libassistant/shared/internal_api/assistant_manager_delegate.h" |
| #include "libassistant/shared/internal_api/assistant_manager_internal.h" |
| #include "libassistant/shared/public/assistant_manager.h" |
| #include "libassistant/shared/public/media_manager.h" |
| #include "mojo/public/mojom/base/time.mojom.h" |
| #include "services/media_session/public/mojom/media_session.mojom.h" |
| #include "services/network/public/cpp/shared_url_loader_factory.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "url/gurl.h" |
| |
| // A macro which ensures we are running on the main thread. |
| #define ENSURE_MAIN_THREAD(method, ...) \ |
| if (!main_task_runner()->RunsTasksInCurrentSequence()) { \ |
| main_task_runner()->PostTask( \ |
| FROM_HERE, \ |
| base::BindOnce(method, weak_factory_.GetWeakPtr(), ##__VA_ARGS__)); \ |
| return; \ |
| } |
| |
| using ActionModule = assistant_client::ActionModule; |
| using Resolution = assistant_client::ConversationStateListener::Resolution; |
| using MediaStatus = assistant_client::MediaStatus; |
| |
| namespace api = ::assistant::api; |
| |
| namespace chromeos { |
| namespace assistant { |
| namespace { |
| |
| static bool is_first_init = true; |
| |
| constexpr char kWiFiDeviceSettingId[] = "WIFI"; |
| constexpr char kBluetoothDeviceSettingId[] = "BLUETOOTH"; |
| constexpr char kVolumeLevelDeviceSettingId[] = "VOLUME_LEVEL"; |
| constexpr char kScreenBrightnessDeviceSettingId[] = "BRIGHTNESS_LEVEL"; |
| constexpr char kDoNotDisturbDeviceSettingId[] = "DO_NOT_DISTURB"; |
| constexpr char kNightLightDeviceSettingId[] = "NIGHT_LIGHT_SWITCH"; |
| constexpr char kIntentActionView[] = "android.intent.action.VIEW"; |
| |
| constexpr base::Feature kChromeOSAssistantDogfood{ |
| "ChromeOSAssistantDogfood", base::FEATURE_DISABLED_BY_DEFAULT}; |
| |
| constexpr char kServersideDogfoodExperimentId[] = "20347368"; |
| constexpr char kServersideOpenAppExperimentId[] = "39651593"; |
| |
| constexpr char kNextTrackClientOp[] = "media.NEXT"; |
| constexpr char kPauseTrackClientOp[] = "media.PAUSE"; |
| constexpr char kPlayMediaClientOp[] = "media.PLAY_MEDIA"; |
| constexpr char kPrevTrackClientOp[] = "media.PREVIOUS"; |
| constexpr char kResumeTrackClientOp[] = "media.RESUME"; |
| constexpr char kStopTrackClientOp[] = "media.STOP"; |
| |
| // The screen context query is locale independent. That is the same query |
| // applies to all locales. |
| constexpr char kScreenContextQuery[] = "screen context"; |
| |
| constexpr float kDefaultSliderStep = 0.1f; |
| |
| bool IsScreenContextAllowed(ash::AssistantStateBase* assistant_state) { |
| return assistant_state->allowed_state() == |
| ash::mojom::AssistantAllowedState::ALLOWED && |
| assistant_state->settings_enabled().value_or(false) && |
| assistant_state->context_enabled().value_or(false); |
| } |
| |
| action::AppStatus GetActionAppStatus(mojom::AppStatus status) { |
| switch (status) { |
| case mojom::AppStatus::UNKNOWN: |
| return action::UNKNOWN; |
| case mojom::AppStatus::AVAILABLE: |
| return action::AVAILABLE; |
| case mojom::AppStatus::UNAVAILABLE: |
| return action::UNAVAILABLE; |
| case mojom::AppStatus::VERSION_MISMATCH: |
| return action::VERSION_MISMATCH; |
| case mojom::AppStatus::DISABLED: |
| return action::DISABLED; |
| } |
| } |
| |
| ash::mojom::AssistantTimerState GetTimerState( |
| assistant_client::Timer::State state) { |
| switch (state) { |
| case assistant_client::Timer::State::UNKNOWN: |
| return ash::mojom::AssistantTimerState::kUnknown; |
| case assistant_client::Timer::State::SCHEDULED: |
| return ash::mojom::AssistantTimerState::kScheduled; |
| case assistant_client::Timer::State::PAUSED: |
| return ash::mojom::AssistantTimerState::kPaused; |
| case assistant_client::Timer::State::FIRED: |
| return ash::mojom::AssistantTimerState::kFired; |
| } |
| } |
| |
| CommunicationErrorType CommunicationErrorTypeFromLibassistantErrorCode( |
| int error_code) { |
| if (IsAuthError(error_code)) |
| return CommunicationErrorType::AuthenticationError; |
| return CommunicationErrorType::Other; |
| } |
| |
| } // namespace |
| |
| AssistantManagerServiceImpl::AssistantManagerServiceImpl( |
| mojom::Client* client, |
| ServiceContext* context, |
| std::unique_ptr<AssistantManagerServiceDelegate> delegate, |
| std::unique_ptr<network::SharedURLLoaderFactoryInfo> |
| url_loader_factory_info, |
| bool is_signed_out_mode) |
| : client_(client), |
| media_session_(std::make_unique<AssistantMediaSession>(client_, this)), |
| action_module_(std::make_unique<action::CrosActionModule>( |
| this, |
| assistant::features::IsAppSupportEnabled(), |
| assistant::features::IsRoutinesEnabled())), |
| chromium_api_delegate_(std::move(url_loader_factory_info)), |
| assistant_settings_manager_( |
| std::make_unique<AssistantSettingsManagerImpl>(context, this)), |
| context_(context), |
| delegate_(std::move(delegate)), |
| background_thread_("background thread"), |
| is_signed_out_mode_(is_signed_out_mode), |
| app_list_subscriber_binding_(this), |
| weak_factory_(this) { |
| background_thread_.Start(); |
| |
| platform_api_ = delegate_->CreatePlatformApi( |
| media_session_.get(), background_thread_.task_runner()); |
| |
| mojo::Remote<media_session::mojom::MediaControllerManager> |
| media_controller_manager; |
| client->RequestMediaControllerManager( |
| media_controller_manager.BindNewPipeAndPassReceiver()); |
| media_controller_manager->CreateActiveMediaController( |
| mojo::MakeRequest(&media_controller_)); |
| } |
| |
| AssistantManagerServiceImpl::~AssistantManagerServiceImpl() { |
| background_thread_.Stop(); |
| } |
| |
| void AssistantManagerServiceImpl::Start( |
| const base::Optional<std::string>& access_token, |
| bool enable_hotword, |
| base::OnceClosure post_init_callback) { |
| DCHECK(!assistant_manager_); |
| DCHECK_EQ(state_, State::STOPPED); |
| |
| // Set the flag to avoid starting the service multiple times. |
| state_ = State::STARTING; |
| |
| started_time_ = base::TimeTicks::Now(); |
| |
| EnableHotword(enable_hotword); |
| |
| // LibAssistant creation will make file IO and sync wait. Post the creation to |
| // background thread to avoid DCHECK. |
| background_thread_.task_runner()->PostTaskAndReply( |
| FROM_HERE, |
| base::BindOnce(&AssistantManagerServiceImpl::StartAssistantInternal, |
| base::Unretained(this), access_token), |
| base::BindOnce(&AssistantManagerServiceImpl::PostInitAssistant, |
| weak_factory_.GetWeakPtr(), |
| std::move(post_init_callback))); |
| } |
| |
| void AssistantManagerServiceImpl::Stop() { |
| // We cannot cleanly stop the service if it is in the process of starting up. |
| DCHECK_NE(state_, State::STARTING); |
| |
| state_ = State::STOPPED; |
| |
| // When user disables the feature, we also deletes all data. |
| if (!assistant_state()->settings_enabled().value() && assistant_manager_) |
| assistant_manager_->ResetAllDataAndShutdown(); |
| |
| media_controller_observer_receiver_.reset(); |
| |
| assistant_manager_internal_ = nullptr; |
| assistant_manager_.reset(nullptr); |
| display_connection_.reset(nullptr); |
| } |
| |
| AssistantManagerService::State AssistantManagerServiceImpl::GetState() const { |
| return state_; |
| } |
| |
| void AssistantManagerServiceImpl::SetAccessToken( |
| const std::string& access_token) { |
| if (!assistant_manager_) |
| return; |
| |
| DCHECK(!access_token.empty()); |
| |
| VLOG(1) << "Set access token."; |
| // Push the |access_token| we got as an argument into AssistantManager before |
| // starting to ensure that all server requests will be authenticated once |
| // it is started. |user_id| is used to pair a user to their |access_token|, |
| // since we do not support multi-user in this example we can set it to a |
| // dummy value like "0". |
| assistant_manager_->SetAuthTokens( |
| {std::pair<std::string, std::string>(kUserID, access_token)}); |
| } |
| |
| void AssistantManagerServiceImpl::RegisterFallbackMediaHandler() { |
| // This is a callback from LibAssistant, it is async from LibAssistant thread. |
| // It is possible that when it reaches here, the assistant_manager_ has |
| // been stopped. |
| if (!assistant_manager_internal_) |
| return; |
| |
| // Register handler for media actions. |
| assistant_manager_internal_->RegisterFallbackMediaHandler( |
| [this](std::string action_name, std::string media_action_args_proto) { |
| if (action_name == kPlayMediaClientOp) { |
| OnPlayMedia(media_action_args_proto); |
| } else { |
| OnMediaControlAction(action_name, media_action_args_proto); |
| } |
| }); |
| } |
| |
| void AssistantManagerServiceImpl::UpdateInternalMediaPlayerStatus( |
| media_session::mojom::MediaSessionAction action) { |
| auto* media_manager = assistant_manager_->GetMediaManager(); |
| if (!media_manager) |
| return; |
| |
| switch (action) { |
| case media_session::mojom::MediaSessionAction::kPause: |
| media_manager->Pause(); |
| break; |
| case media_session::mojom::MediaSessionAction::kPlay: |
| media_manager->Resume(); |
| break; |
| case media_session::mojom::MediaSessionAction::kPreviousTrack: |
| case media_session::mojom::MediaSessionAction::kNextTrack: |
| case media_session::mojom::MediaSessionAction::kSeekBackward: |
| case media_session::mojom::MediaSessionAction::kSeekForward: |
| case media_session::mojom::MediaSessionAction::kSkipAd: |
| case media_session::mojom::MediaSessionAction::kStop: |
| case media_session::mojom::MediaSessionAction::kSeekTo: |
| case media_session::mojom::MediaSessionAction::kScrubTo: |
| NOTIMPLEMENTED(); |
| break; |
| } |
| } |
| |
| void AssistantManagerServiceImpl::WaitUntilStartIsFinishedForTesting() { |
| // First we wait until |StartAssistantInternal| is finished. |
| background_thread_.FlushForTesting(); |
| // Then we wait until |PostInitAssistant| finishes. |
| // (which runs on the main thread). |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| void AssistantManagerServiceImpl::AddMediaControllerObserver() { |
| if (features::IsMediaSessionIntegrationEnabled()) { |
| media_controller_->AddObserver( |
| media_controller_observer_receiver_.BindNewPipeAndPassRemote()); |
| } |
| } |
| |
| void AssistantManagerServiceImpl::RegisterAlarmsTimersListener() { |
| if (!assistant_manager_internal_) |
| return; |
| |
| auto* alarm_timer_manager = |
| assistant_manager_internal_->GetAlarmTimerManager(); |
| |
| // Can be nullptr during unittests. |
| if (!alarm_timer_manager) |
| return; |
| |
| alarm_timer_manager->RegisterRingingStateListener([this]() { |
| main_task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| &AssistantManagerServiceImpl::OnAlarmTimerStateChangedOnMainThread, |
| weak_factory_.GetWeakPtr())); |
| }); |
| } |
| |
| void AssistantManagerServiceImpl::EnableListening(bool enable) { |
| if (!assistant_manager_) |
| return; |
| assistant_manager_->EnableListening(enable); |
| } |
| |
| void AssistantManagerServiceImpl::EnableHotword(bool enable) { |
| platform_api_->OnHotwordEnabled(enable); |
| } |
| |
| void AssistantManagerServiceImpl::SetArcPlayStoreEnabled(bool enable) { |
| if (!HasStartFinished()) { |
| // Skip setting play store status if libassistant is not ready. The status |
| // will be set when it is ready. |
| return; |
| } |
| // Both LibAssistant and Chrome threads may access |display_connection_|. |
| // |display_connection_| is thread safe. |
| if (assistant::features::IsAppSupportEnabled()) |
| display_connection_->SetArcPlayStoreEnabled(enable); |
| } |
| |
| AssistantSettingsManager* |
| AssistantManagerServiceImpl::GetAssistantSettingsManager() { |
| return assistant_settings_manager_.get(); |
| } |
| |
| void AssistantManagerServiceImpl::AddCommunicationErrorObserver( |
| AssistantCommunicationErrorObserver* observer) { |
| error_observers_.AddObserver(observer); |
| } |
| |
| void AssistantManagerServiceImpl::RemoveCommunicationErrorObserver( |
| AssistantCommunicationErrorObserver* observer) { |
| error_observers_.RemoveObserver(observer); |
| } |
| |
| void AssistantManagerServiceImpl::StartVoiceInteraction() { |
| platform_api_->SetMicState(true); |
| assistant_manager_->StartAssistantInteraction(); |
| } |
| |
| void AssistantManagerServiceImpl::StopActiveInteraction( |
| bool cancel_conversation) { |
| platform_api_->SetMicState(false); |
| |
| if (!assistant_manager_internal_) { |
| VLOG(1) << "Stopping interaction without assistant manager."; |
| return; |
| } |
| assistant_manager_internal_->StopAssistantInteractionInternal( |
| cancel_conversation); |
| } |
| |
| void AssistantManagerServiceImpl::StartWarmerWelcomeInteraction( |
| int num_warmer_welcome_triggered, |
| bool allow_tts) { |
| DCHECK(assistant_manager_internal_ != nullptr); |
| |
| const std::string interaction = |
| CreateWarmerWelcomeInteraction(num_warmer_welcome_triggered); |
| |
| assistant_client::VoicelessOptions options; |
| options.is_user_initiated = true; |
| options.modality = |
| allow_tts ? assistant_client::VoicelessOptions::Modality::VOICE_MODALITY |
| : assistant_client::VoicelessOptions::Modality::TYPING_MODALITY; |
| |
| assistant_manager_internal_->SendVoicelessInteraction( |
| interaction, /*description=*/"warmer_welcome_trigger", options, |
| [](auto) {}); |
| } |
| |
| // TODO(eyor): Add a method that can be called to clear the cached interaction |
| // when the UI is hidden/closed. |
| void AssistantManagerServiceImpl::StartCachedScreenContextInteraction() { |
| if (!IsScreenContextAllowed(assistant_state())) |
| return; |
| |
| // It is illegal to call this method without having first cached screen |
| // context (see CacheScreenContext()). |
| DCHECK(assistant_extra_); |
| DCHECK(assistant_tree_); |
| DCHECK(!assistant_screenshot_.empty()); |
| |
| SendScreenContextRequest(assistant_extra_.get(), assistant_tree_.get(), |
| assistant_screenshot_); |
| } |
| |
| void AssistantManagerServiceImpl::StartEditReminderInteraction( |
| const std::string& client_id) { |
| const std::string interaction = CreateEditReminderInteraction(client_id); |
| assistant_client::VoicelessOptions voiceless_options; |
| |
| voiceless_options.is_user_initiated = true; |
| assistant_manager_internal_->SendVoicelessInteraction( |
| interaction, std::string(), voiceless_options, [](auto) {}); |
| } |
| |
| void AssistantManagerServiceImpl::StartMetalayerInteraction( |
| const gfx::Rect& region) { |
| if (!IsScreenContextAllowed(assistant_state())) |
| return; |
| |
| assistant_screen_context_controller()->RequestScreenshot( |
| region, |
| base::BindOnce(&AssistantManagerServiceImpl::SendScreenContextRequest, |
| weak_factory_.GetWeakPtr(), /*assistant_extra=*/nullptr, |
| /*assistant_tree=*/nullptr)); |
| } |
| |
| void AssistantManagerServiceImpl::StartTextInteraction(const std::string& query, |
| bool allow_tts) { |
| assistant_client::VoicelessOptions options; |
| options.is_user_initiated = true; |
| |
| if (!allow_tts) { |
| options.modality = |
| assistant_client::VoicelessOptions::Modality::TYPING_MODALITY; |
| } |
| |
| // Cache metadata about this interaction that can be resolved when the |
| // associated conversation turn starts in LibAssistant. |
| options.conversation_turn_id = base::NumberToString(next_interaction_id_++); |
| pending_interactions_[options.conversation_turn_id] = |
| mojom::AssistantInteractionMetadata::New( |
| /*type=*/mojom::AssistantInteractionType::kText, /*query=*/query); |
| |
| if (base::FeatureList::IsEnabled( |
| assistant::features::kEnableTextQueriesWithClientDiscourseContext) && |
| assistant_extra_ && assistant_tree_) { |
| // We don't send the screenshot, because the backend only needs the |
| // view hierarchy to resolve contextual queries such as "Who is he?". |
| assistant_manager_internal_->SendTextQueryWithClientDiscourseContext( |
| query, |
| CreateContextProto( |
| AssistantBundle{assistant_extra_.get(), assistant_tree_.get()}, |
| is_first_client_discourse_context_query_), |
| options); |
| is_first_client_discourse_context_query_ = false; |
| } else { |
| std::string interaction = CreateTextQueryInteraction(query); |
| assistant_manager_internal_->SendVoicelessInteraction( |
| interaction, /*description=*/"text_query", options, [](auto) {}); |
| } |
| } |
| |
| void AssistantManagerServiceImpl::AddAssistantInteractionSubscriber( |
| mojo::PendingRemote<mojom::AssistantInteractionSubscriber> subscriber) { |
| mojo::Remote<mojom::AssistantInteractionSubscriber> subscriber_remote( |
| std::move(subscriber)); |
| interaction_subscribers_.Add(std::move(subscriber_remote)); |
| } |
| |
| void AssistantManagerServiceImpl::RetrieveNotification( |
| mojom::AssistantNotificationPtr notification, |
| int action_index) { |
| const std::string& notification_id = notification->server_id; |
| const std::string& consistency_token = notification->consistency_token; |
| const std::string& opaque_token = notification->opaque_token; |
| |
| const std::string request_interaction = |
| SerializeNotificationRequestInteraction( |
| notification_id, consistency_token, opaque_token, action_index); |
| |
| assistant_client::VoicelessOptions options; |
| options.is_user_initiated = true; |
| |
| assistant_manager_internal_->SendVoicelessInteraction( |
| request_interaction, "RequestNotification", options, [](auto) {}); |
| } |
| |
| void AssistantManagerServiceImpl::DismissNotification( |
| mojom::AssistantNotificationPtr notification) { |
| const std::string& notification_id = notification->server_id; |
| const std::string& consistency_token = notification->consistency_token; |
| const std::string& opaque_token = notification->opaque_token; |
| const std::string& grouping_key = notification->grouping_key; |
| |
| const std::string dismissed_interaction = |
| SerializeNotificationDismissedInteraction( |
| notification_id, consistency_token, opaque_token, {grouping_key}); |
| |
| assistant_client::VoicelessOptions options; |
| options.obfuscated_gaia_id = notification->obfuscated_gaia_id; |
| |
| assistant_manager_internal_->SendVoicelessInteraction( |
| dismissed_interaction, "DismissNotification", options, [](auto) {}); |
| } |
| |
| void AssistantManagerServiceImpl::OnConversationTurnStartedInternal( |
| const assistant_client::ConversationTurnMetadata& metadata) { |
| main_task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| &AssistantManagerServiceImpl::OnConversationTurnStartedOnMainThread, |
| weak_factory_.GetWeakPtr(), metadata)); |
| } |
| |
| void AssistantManagerServiceImpl::OnConversationTurnFinished( |
| Resolution resolution) { |
| main_task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| &AssistantManagerServiceImpl::OnConversationTurnFinishedOnMainThread, |
| weak_factory_.GetWeakPtr(), resolution)); |
| } |
| |
| void AssistantManagerServiceImpl::OnScheduleWait(int id, int time_ms) { |
| ENSURE_MAIN_THREAD(&AssistantManagerServiceImpl::OnScheduleWait, id, time_ms); |
| |
| // Schedule a wait for |time_ms|, notifying the CrosActionModule when the wait |
| // has finished so that it can inform LibAssistant to resume execution. |
| main_task_runner()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce( |
| [](const base::WeakPtr<AssistantManagerServiceImpl>& weak_ptr, |
| int id) { |
| if (weak_ptr) { |
| weak_ptr->action_module_->OnScheduledWaitDone( |
| id, /*cancelled=*/false); |
| } |
| }, |
| weak_factory_.GetWeakPtr(), id), |
| base::TimeDelta::FromMilliseconds(time_ms)); |
| |
| // Notify subscribers that a wait has been started. |
| for (auto& it : interaction_subscribers_) |
| it->OnWaitStarted(); |
| } |
| |
| // TODO(b/113541754): Deprecate this API when the server provides a fallback. |
| void AssistantManagerServiceImpl::OnShowContextualQueryFallback() { |
| // Show fallback text. |
| main_task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&AssistantManagerServiceImpl::OnShowTextOnMainThread, |
| weak_factory_.GetWeakPtr(), |
| l10n_util::GetStringUTF8( |
| IDS_ASSISTANT_SCREEN_CONTEXT_QUERY_FALLBACK_TEXT))); |
| |
| // Construct a fallback card. |
| std::stringstream html; |
| html << R"( |
| <html> |
| <head><meta CHARSET='utf-8'></head> |
| <body> |
| <style> |
| * { |
| cursor: default; |
| font-family: Google Sans, sans-serif; |
| user-select: none; |
| } |
| html, body { margin: 0; padding: 0; } |
| div { |
| border: 1px solid rgba(32, 33, 36, 0.08); |
| border-radius: 12px; |
| color: #5F6368; |
| font-size: 13px; |
| margin: 1px; |
| padding: 16px; |
| text-align: center; |
| } |
| </style> |
| <div>)" |
| << l10n_util::GetStringUTF8( |
| IDS_ASSISTANT_SCREEN_CONTEXT_QUERY_FALLBACK_CARD) |
| << "</div></body></html>"; |
| |
| // Show fallback card. |
| main_task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&AssistantManagerServiceImpl::OnShowHtmlOnMainThread, |
| weak_factory_.GetWeakPtr(), html.str(), /*fallback=*/"")); |
| } |
| |
| void AssistantManagerServiceImpl::OnShowHtml(const std::string& html, |
| const std::string& fallback) { |
| main_task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&AssistantManagerServiceImpl::OnShowHtmlOnMainThread, |
| weak_factory_.GetWeakPtr(), html, fallback)); |
| } |
| |
| void AssistantManagerServiceImpl::OnShowSuggestions( |
| const std::vector<action::Suggestion>& suggestions) { |
| // Convert to mojom struct for IPC. |
| std::vector<mojom::AssistantSuggestionPtr> ptrs; |
| for (const action::Suggestion& suggestion : suggestions) { |
| mojom::AssistantSuggestionPtr ptr = mojom::AssistantSuggestion::New(); |
| ptr->text = suggestion.text; |
| ptr->icon_url = GURL(suggestion.icon_url); |
| ptr->action_url = GURL(suggestion.action_url); |
| ptrs.push_back(std::move(ptr)); |
| } |
| |
| main_task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| &AssistantManagerServiceImpl::OnShowSuggestionsOnMainThread, |
| weak_factory_.GetWeakPtr(), std::move(ptrs))); |
| } |
| |
| void AssistantManagerServiceImpl::OnShowText(const std::string& text) { |
| main_task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&AssistantManagerServiceImpl::OnShowTextOnMainThread, |
| weak_factory_.GetWeakPtr(), text)); |
| } |
| |
| void AssistantManagerServiceImpl::OnOpenUrl(const std::string& url, |
| bool is_background) { |
| main_task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&AssistantManagerServiceImpl::OnOpenUrlOnMainThread, |
| weak_factory_.GetWeakPtr(), url, is_background)); |
| } |
| |
| void AssistantManagerServiceImpl::OnShowNotification( |
| const action::Notification& notification) { |
| mojom::AssistantNotificationPtr notification_ptr = |
| mojom::AssistantNotification::New(); |
| notification_ptr->title = notification.title; |
| notification_ptr->message = notification.text; |
| notification_ptr->action_url = GURL(notification.action_url); |
| notification_ptr->client_id = notification.notification_id; |
| notification_ptr->server_id = notification.notification_id; |
| notification_ptr->consistency_token = notification.consistency_token; |
| notification_ptr->opaque_token = notification.opaque_token; |
| notification_ptr->grouping_key = notification.grouping_key; |
| notification_ptr->obfuscated_gaia_id = notification.obfuscated_gaia_id; |
| |
| if (notification.expiry_timestamp_ms) { |
| notification_ptr->expiry_time = |
| base::Time::FromJavaTime(notification.expiry_timestamp_ms); |
| } |
| |
| // The server sometimes sends an empty |notification_id|, but our client |
| // requires a non-empty |client_id| for notifications. Known instances in |
| // which the server sends an empty |notification_id| are for Reminders. |
| if (notification_ptr->client_id.empty()) |
| notification_ptr->client_id = base::UnguessableToken::Create().ToString(); |
| |
| for (const auto& button : notification.buttons) { |
| notification_ptr->buttons.push_back(mojom::AssistantNotificationButton::New( |
| button.label, GURL(button.action_url))); |
| } |
| |
| main_task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| &AssistantManagerServiceImpl::OnShowNotificationOnMainThread, |
| weak_factory_.GetWeakPtr(), std::move(notification_ptr))); |
| } |
| |
| void AssistantManagerServiceImpl::OnOpenAndroidApp( |
| const action::AndroidAppInfo& app_info, |
| const action::InteractionInfo& interaction) { |
| ENSURE_MAIN_THREAD(&AssistantManagerServiceImpl::OnOpenAndroidApp, app_info, |
| interaction); |
| mojom::AndroidAppInfoPtr app_info_ptr = mojom::AndroidAppInfo::New(); |
| app_info_ptr->package_name = app_info.package_name; |
| for (auto& it : interaction_subscribers_) { |
| it->OnOpenAppResponse( |
| mojo::Clone(app_info_ptr), |
| base::BindOnce( |
| &AssistantManagerServiceImpl::HandleOpenAndroidAppResponse, |
| weak_factory_.GetWeakPtr(), interaction)); |
| } |
| } |
| |
| void AssistantManagerServiceImpl::OnVerifyAndroidApp( |
| const std::vector<action::AndroidAppInfo>& apps_info, |
| const action::InteractionInfo& interaction) { |
| ENSURE_MAIN_THREAD(&AssistantManagerServiceImpl::OnVerifyAndroidApp, |
| apps_info, interaction); |
| std::vector<mojom::AndroidAppInfoPtr> apps_info_list; |
| for (auto& app_info : apps_info) { |
| mojom::AndroidAppInfoPtr app_info_ptr = mojom::AndroidAppInfo::New(); |
| app_info_ptr->package_name = app_info.package_name; |
| apps_info_list.push_back(std::move(app_info_ptr)); |
| } |
| device_actions()->VerifyAndroidApp( |
| std::move(apps_info_list), |
| base::BindOnce( |
| &AssistantManagerServiceImpl::HandleVerifyAndroidAppResponse, |
| weak_factory_.GetWeakPtr(), interaction)); |
| } |
| |
| void AssistantManagerServiceImpl::OnOpenMediaAndroidIntentOnMainThread( |
| const std::string play_media_args_proto, |
| action::AndroidAppInfo* android_app_info) { |
| // Handle android media playback intent. |
| mojom::AndroidAppInfoPtr app_info_ptr = mojom::AndroidAppInfo::New(); |
| app_info_ptr->package_name = android_app_info->package_name; |
| app_info_ptr->action = kIntentActionView; |
| if (!android_app_info->intent.empty()) { |
| app_info_ptr->intent = android_app_info->intent; |
| } else { |
| std::string url = GetAndroidIntentUrlFromMediaArgs(play_media_args_proto); |
| if (!url.empty()) { |
| app_info_ptr->intent = url; |
| } |
| } |
| for (auto& it : interaction_subscribers_) { |
| it->OnOpenAppResponse( |
| mojo::Clone(app_info_ptr), |
| base::BindOnce( |
| &AssistantManagerServiceImpl::HandleLaunchMediaIntentResponse, |
| weak_factory_.GetWeakPtr())); |
| } |
| } |
| |
| void AssistantManagerServiceImpl::OnPlayMedia( |
| const std::string play_media_args_proto) { |
| ENSURE_MAIN_THREAD(&AssistantManagerServiceImpl::OnPlayMedia, |
| play_media_args_proto); |
| |
| std::unique_ptr<action::AndroidAppInfo> android_app_info = |
| GetAndroidAppInfoFromMediaArgs(play_media_args_proto); |
| if (android_app_info) { |
| OnOpenMediaAndroidIntentOnMainThread(play_media_args_proto, |
| android_app_info.get()); |
| } else { |
| std::string url = GetWebUrlFromMediaArgs(play_media_args_proto); |
| // Fallack to web URL. |
| if (!url.empty()) |
| OnOpenUrlOnMainThread(url, /*in_background=*/false); |
| } |
| } |
| |
| void AssistantManagerServiceImpl::OnMediaControlAction( |
| const std::string& action_name, |
| const std::string& media_action_args_proto) { |
| ENSURE_MAIN_THREAD(&AssistantManagerServiceImpl::OnMediaControlAction, |
| action_name, media_action_args_proto); |
| |
| if (action_name == kPauseTrackClientOp) { |
| media_controller_->Suspend(); |
| return; |
| } |
| |
| if (action_name == kResumeTrackClientOp) { |
| media_controller_->Resume(); |
| return; |
| } |
| |
| if (action_name == kNextTrackClientOp) { |
| media_controller_->NextTrack(); |
| return; |
| } |
| |
| if (action_name == kPrevTrackClientOp) { |
| media_controller_->PreviousTrack(); |
| return; |
| } |
| |
| if (action_name == kStopTrackClientOp) { |
| media_controller_->Stop(); |
| return; |
| } |
| // TODO(llin): Handle media.SEEK_RELATIVE. |
| } |
| |
| void AssistantManagerServiceImpl::OnRecognitionStateChanged( |
| assistant_client::ConversationStateListener::RecognitionState state, |
| const assistant_client::ConversationStateListener::RecognitionResult& |
| recognition_result) { |
| main_task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| &AssistantManagerServiceImpl::OnRecognitionStateChangedOnMainThread, |
| weak_factory_.GetWeakPtr(), state, recognition_result)); |
| } |
| |
| void AssistantManagerServiceImpl::OnRespondingStarted(bool is_error_response) { |
| main_task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| &AssistantManagerServiceImpl::OnRespondingStartedOnMainThread, |
| weak_factory_.GetWeakPtr(), is_error_response)); |
| } |
| |
| void AssistantManagerServiceImpl::OnSpeechLevelUpdated( |
| const float speech_level) { |
| main_task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| &AssistantManagerServiceImpl::OnSpeechLevelUpdatedOnMainThread, |
| weak_factory_.GetWeakPtr(), speech_level)); |
| } |
| |
| void LogUnsupportedChange(api::client_op::ModifySettingArgs args) { |
| LOG(ERROR) << "Unsupported change operation: " << args.change() |
| << " for setting " << args.setting_id(); |
| } |
| |
| void HandleOnOffChange(api::client_op::ModifySettingArgs modify_setting_args, |
| std::function<void(bool)> on_off_handler) { |
| switch (modify_setting_args.change()) { |
| case api::client_op::ModifySettingArgs_Change_ON: |
| on_off_handler(true); |
| return; |
| case api::client_op::ModifySettingArgs_Change_OFF: |
| on_off_handler(false); |
| return; |
| |
| // Currently there are no use-cases for toggling. This could change in the |
| // future. |
| case api::client_op::ModifySettingArgs_Change_TOGGLE: |
| break; |
| |
| case api::client_op::ModifySettingArgs_Change_SET: |
| case api::client_op::ModifySettingArgs_Change_INCREASE: |
| case api::client_op::ModifySettingArgs_Change_DECREASE: |
| case api::client_op::ModifySettingArgs_Change_UNSPECIFIED: |
| // This shouldn't happen. |
| break; |
| } |
| LogUnsupportedChange(modify_setting_args); |
| } |
| |
| // Helper function that converts a slider value sent from the server, either |
| // absolute or a delta, from a given unit (e.g., STEP), to a percentage. |
| double ConvertSliderValueToLevel(double value, |
| api::client_op::ModifySettingArgs_Unit unit, |
| double default_value) { |
| switch (unit) { |
| case api::client_op::ModifySettingArgs_Unit_RANGE: |
| // "set volume to 20%". |
| return value; |
| case api::client_op::ModifySettingArgs_Unit_STEP: |
| // "set volume to 20". Treat the step as a percentage. |
| return value / 100.0f; |
| |
| // Currently, factor (e.g., 'double the volume') and decibel units aren't |
| // handled by the backend. This could change in the future. |
| case api::client_op::ModifySettingArgs_Unit_FACTOR: |
| case api::client_op::ModifySettingArgs_Unit_DECIBEL: |
| break; |
| |
| case api::client_op::ModifySettingArgs_Unit_NATIVE: |
| case api::client_op::ModifySettingArgs_Unit_UNKNOWN_UNIT: |
| // This shouldn't happen. |
| break; |
| } |
| LOG(ERROR) << "Unsupported slider unit: " << unit; |
| return default_value; |
| } |
| |
| void HandleSliderChange(api::client_op::ModifySettingArgs modify_setting_args, |
| std::function<void(double)> set_value_handler, |
| std::function<double()> get_value_handler) { |
| switch (modify_setting_args.change()) { |
| case api::client_op::ModifySettingArgs_Change_SET: { |
| // For unsupported units, set the value to the current value, for |
| // visual feedback. |
| double new_value = ConvertSliderValueToLevel( |
| modify_setting_args.numeric_value(), modify_setting_args.unit(), |
| get_value_handler()); |
| set_value_handler(new_value); |
| return; |
| } |
| |
| case api::client_op::ModifySettingArgs_Change_INCREASE: |
| case api::client_op::ModifySettingArgs_Change_DECREASE: { |
| double current_value = get_value_handler(); |
| double step = kDefaultSliderStep; |
| if (modify_setting_args.numeric_value() != 0.0f) { |
| // For unsupported units, use the default step percentage. |
| step = ConvertSliderValueToLevel(modify_setting_args.numeric_value(), |
| modify_setting_args.unit(), |
| kDefaultSliderStep); |
| } |
| double new_value = (modify_setting_args.change() == |
| api::client_op::ModifySettingArgs_Change_INCREASE) |
| ? std::min(current_value + step, 1.0) |
| : std::max(current_value - step, 0.0); |
| set_value_handler(new_value); |
| return; |
| } |
| |
| case api::client_op::ModifySettingArgs_Change_ON: |
| case api::client_op::ModifySettingArgs_Change_OFF: |
| case api::client_op::ModifySettingArgs_Change_TOGGLE: |
| case api::client_op::ModifySettingArgs_Change_UNSPECIFIED: |
| // This shouldn't happen. |
| break; |
| } |
| LogUnsupportedChange(modify_setting_args); |
| } |
| |
| void AssistantManagerServiceImpl::OnModifySettingsAction( |
| const std::string& modify_setting_args_proto) { |
| api::client_op::ModifySettingArgs modify_setting_args; |
| modify_setting_args.ParseFromString(modify_setting_args_proto); |
| DCHECK(IsSettingSupported(modify_setting_args.setting_id())); |
| receive_modify_settings_proto_response_ = true; |
| |
| if (modify_setting_args.setting_id() == kWiFiDeviceSettingId) { |
| HandleOnOffChange(modify_setting_args, [&](bool enabled) { |
| this->device_actions()->SetWifiEnabled(enabled); |
| }); |
| } |
| |
| if (modify_setting_args.setting_id() == kBluetoothDeviceSettingId) { |
| HandleOnOffChange(modify_setting_args, [&](bool enabled) { |
| this->device_actions()->SetBluetoothEnabled(enabled); |
| }); |
| } |
| |
| if (modify_setting_args.setting_id() == kVolumeLevelDeviceSettingId) { |
| assistant_client::VolumeControl& volume_control = |
| this->platform_api_->GetAudioOutputProvider().GetVolumeControl(); |
| |
| HandleSliderChange( |
| modify_setting_args, |
| [&](double value) { volume_control.SetSystemVolume(value, true); }, |
| [&]() { return volume_control.GetSystemVolume(); }); |
| } |
| |
| if (modify_setting_args.setting_id() == kScreenBrightnessDeviceSettingId) { |
| this->device_actions()->GetScreenBrightnessLevel(base::BindOnce( |
| [](base::WeakPtr<chromeos::assistant::AssistantManagerServiceImpl> |
| this_, |
| api::client_op::ModifySettingArgs modify_setting_args, bool success, |
| double current_value) { |
| if (!success || !this_) { |
| return; |
| } |
| HandleSliderChange( |
| modify_setting_args, |
| [&](double new_value) { |
| this_->device_actions()->SetScreenBrightnessLevel(new_value, |
| true); |
| }, |
| [&]() { return current_value; }); |
| }, |
| weak_factory_.GetWeakPtr(), modify_setting_args)); |
| } |
| |
| if (modify_setting_args.setting_id() == kDoNotDisturbDeviceSettingId) { |
| HandleOnOffChange(modify_setting_args, [&](bool enabled) { |
| this->assistant_notification_controller()->SetQuietMode(enabled); |
| }); |
| } |
| |
| if (modify_setting_args.setting_id() == kNightLightDeviceSettingId) { |
| HandleOnOffChange(modify_setting_args, [&](bool enabled) { |
| this->device_actions()->SetNightLightEnabled(enabled); |
| }); |
| } |
| } |
| |
| ActionModule::Result AssistantManagerServiceImpl::HandleModifySettingClientOp( |
| const std::string& modify_setting_args_proto) { |
| DVLOG(2) << "HandleModifySettingClientOp=" << modify_setting_args_proto; |
| main_task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&AssistantManagerServiceImpl::OnModifySettingsAction, |
| weak_factory_.GetWeakPtr(), modify_setting_args_proto)); |
| return ActionModule::Result::Ok(); |
| } |
| |
| bool AssistantManagerServiceImpl::IsSettingSupported( |
| const std::string& setting_id) { |
| DVLOG(2) << "IsSettingSupported=" << setting_id; |
| return (setting_id == kWiFiDeviceSettingId || |
| setting_id == kBluetoothDeviceSettingId || |
| setting_id == kVolumeLevelDeviceSettingId || |
| setting_id == kScreenBrightnessDeviceSettingId || |
| setting_id == kDoNotDisturbDeviceSettingId || |
| setting_id == kNightLightDeviceSettingId); |
| } |
| |
| bool AssistantManagerServiceImpl::SupportsModifySettings() { |
| return true; |
| } |
| |
| void AssistantManagerServiceImpl::OnNotificationRemoved( |
| const std::string& grouping_key) { |
| main_task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| &AssistantManagerServiceImpl::OnNotificationRemovedOnMainThread, |
| weak_factory_.GetWeakPtr(), grouping_key)); |
| } |
| |
| void AssistantManagerServiceImpl::OnCommunicationError(int error_code) { |
| main_task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| &AssistantManagerServiceImpl::OnCommunicationErrorOnMainThread, |
| weak_factory_.GetWeakPtr(), error_code)); |
| } |
| |
| void AssistantManagerServiceImpl::StartAssistantInternal( |
| const base::Optional<std::string>& access_token) { |
| DCHECK(background_thread_.task_runner()->BelongsToCurrentThread()); |
| base::AutoLock lock(new_assistant_manager_lock_); |
| // There can only be one |AssistantManager| instance at any given time. |
| DCHECK(!assistant_manager_); |
| new_display_connection_ = std::make_unique<CrosDisplayConnection>( |
| this, assistant::features::IsFeedbackUiEnabled(), |
| assistant::features::IsMediaSessionIntegrationEnabled()); |
| |
| new_assistant_manager_ = delegate_->CreateAssistantManager( |
| platform_api_.get(), CreateLibAssistantConfig()); |
| new_assistant_manager_internal_ = |
| delegate_->UnwrapAssistantManagerInternal(new_assistant_manager_.get()); |
| |
| UpdateInternalOptions(new_assistant_manager_internal_); |
| |
| new_assistant_manager_internal_->SetDisplayConnection( |
| new_display_connection_.get()); |
| new_assistant_manager_internal_->RegisterActionModule(action_module_.get()); |
| new_assistant_manager_internal_->SetAssistantManagerDelegate(this); |
| new_assistant_manager_internal_->GetFuchsiaApiHelperOrDie() |
| ->SetFuchsiaApiDelegate(&chromium_api_delegate_); |
| new_assistant_manager_->AddConversationStateListener(this); |
| new_assistant_manager_->AddDeviceStateListener(this); |
| |
| std::vector<std::string> server_experiment_ids; |
| FillServerExperimentIds(&server_experiment_ids); |
| |
| if (server_experiment_ids.size() > 0) { |
| new_assistant_manager_internal_->AddExtraExperimentIds( |
| server_experiment_ids); |
| } |
| |
| if (!is_signed_out_mode_) { |
| new_assistant_manager_->SetAuthTokens( |
| {std::pair<std::string, std::string>(kUserID, access_token.value())}); |
| } |
| new_assistant_manager_->Start(); |
| } |
| |
| void AssistantManagerServiceImpl::PostInitAssistant( |
| base::OnceClosure post_init_callback) { |
| DCHECK(main_task_runner()->RunsTasksInCurrentSequence()); |
| DCHECK_EQ(state_, State::STARTING); |
| |
| { |
| base::AutoLock lock(new_assistant_manager_lock_); |
| |
| // It is possible that multiple |StartAssistantInternal| finished on |
| // background thread, before any of the matching |PostInitAssistant| had |
| // run. Because we only hold the last created instance in |
| // |new_assistant_manager_|, it is possible that |new_assistant_manager_| be |
| // null if we moved it in previous |PostInitAssistant| runs. |
| if (!new_assistant_manager_) { |
| std::move(post_init_callback).Run(); |
| return; |
| } |
| |
| display_connection_ = std::move(new_display_connection_); |
| assistant_manager_ = std::move(new_assistant_manager_); |
| assistant_manager_internal_ = new_assistant_manager_internal_; |
| new_assistant_manager_internal_ = nullptr; |
| } |
| |
| state_ = State::STARTED; |
| |
| const base::TimeDelta time_since_started = |
| base::TimeTicks::Now() - started_time_; |
| UMA_HISTOGRAM_TIMES("Assistant.ServiceStartTime", time_since_started); |
| |
| std::move(post_init_callback).Run(); |
| assistant_settings_manager_->UpdateServerDeviceSettings(); |
| |
| if (base::FeatureList::IsEnabled(assistant::features::kAssistantAppSupport)) { |
| mojom::AppListEventSubscriberPtr subscriber_ptr; |
| app_list_subscriber_binding_.Bind(mojo::MakeRequest(&subscriber_ptr)); |
| device_actions()->AddAppListEventSubscriber(std::move(subscriber_ptr)); |
| } |
| } |
| |
| void AssistantManagerServiceImpl::HandleLaunchMediaIntentResponse( |
| bool app_opened) { |
| // TODO(llin): Handle the response. |
| NOTIMPLEMENTED(); |
| } |
| |
| void AssistantManagerServiceImpl::HandleOpenAndroidAppResponse( |
| const action::InteractionInfo& interaction, |
| bool app_opened) { |
| std::string interaction_proto = CreateOpenProviderResponseInteraction( |
| interaction.interaction_id, app_opened); |
| |
| assistant_client::VoicelessOptions options; |
| options.obfuscated_gaia_id = interaction.user_id; |
| |
| assistant_manager_internal_->SendVoicelessInteraction( |
| interaction_proto, "open_provider_response", options, [](auto) {}); |
| } |
| |
| void AssistantManagerServiceImpl::HandleVerifyAndroidAppResponse( |
| const action::InteractionInfo& interaction, |
| std::vector<mojom::AndroidAppInfoPtr> apps_info) { |
| std::vector<action::AndroidAppInfo> action_apps_info; |
| for (const auto& app_info : apps_info) { |
| action_apps_info.push_back({app_info->package_name, app_info->version, |
| app_info->localized_app_name, app_info->intent, |
| GetActionAppStatus(app_info->status)}); |
| } |
| std::string interaction_proto = CreateVerifyProviderResponseInteraction( |
| interaction.interaction_id, action_apps_info); |
| |
| assistant_client::VoicelessOptions options; |
| options.obfuscated_gaia_id = interaction.user_id; |
| // Set the request to be user initiated so that a new conversation will be |
| // created to handle the client OPs in the response of this request. |
| options.is_user_initiated = true; |
| |
| assistant_manager_internal_->SendVoicelessInteraction( |
| interaction_proto, "verify_provider_response", options, [](auto) {}); |
| } |
| |
| // This method runs on the LibAssistant thread. |
| // This method is triggered as the callback of libassistant bootup checkin. |
| void AssistantManagerServiceImpl::OnStartFinished() { |
| ENSURE_MAIN_THREAD(&AssistantManagerServiceImpl::OnStartFinished); |
| |
| // It is possible the |assistant_manager_| was destructed before the |
| // rescheduled main thread task got a chance to run. We check this and also |
| // try to avoid double run by check |HasStartFinished()|. |
| if (!assistant_manager_ || HasStartFinished()) |
| return; |
| |
| SetStartFinished(); |
| |
| if (is_first_init) { |
| is_first_init = false; |
| // Only sync status at the first init to prevent unexpected corner cases. |
| if (assistant_state()->hotword_enabled().value()) |
| assistant_settings_manager_->SyncSpeakerIdEnrollmentStatus(); |
| } |
| |
| RegisterFallbackMediaHandler(); |
| AddMediaControllerObserver(); |
| |
| auto* media_manager = assistant_manager_->GetMediaManager(); |
| if (media_manager) |
| media_manager->AddListener(this); |
| |
| if (assistant_state()->arc_play_store_enabled().has_value()) |
| SetArcPlayStoreEnabled(assistant_state()->arc_play_store_enabled().value()); |
| |
| if (assistant::features::IsAlarmTimerManagerEnabled()) { |
| RegisterAlarmsTimersListener(); |
| } |
| } |
| |
| void AssistantManagerServiceImpl::OnTimerSoundingStarted() { |
| if (assistant::features::IsAlarmTimerManagerEnabled()) |
| return; |
| |
| ENSURE_MAIN_THREAD(&AssistantManagerServiceImpl::OnTimerSoundingStarted); |
| if (assistant_alarm_timer_controller()) |
| assistant_alarm_timer_controller()->OnTimerSoundingStarted(); |
| } |
| |
| void AssistantManagerServiceImpl::OnTimerSoundingFinished() { |
| if (assistant::features::IsAlarmTimerManagerEnabled()) |
| return; |
| |
| ENSURE_MAIN_THREAD(&AssistantManagerServiceImpl::OnTimerSoundingFinished); |
| if (assistant_alarm_timer_controller()) |
| assistant_alarm_timer_controller()->OnTimerSoundingFinished(); |
| } |
| |
| void AssistantManagerServiceImpl::OnAndroidAppListRefreshed( |
| std::vector<mojom::AndroidAppInfoPtr> apps_info) { |
| std::vector<action::AndroidAppInfo> android_apps_info; |
| for (const auto& app_info : apps_info) { |
| android_apps_info.push_back({app_info->package_name, app_info->version, |
| app_info->localized_app_name, |
| app_info->intent}); |
| } |
| display_connection_->OnAndroidAppListRefreshed(android_apps_info); |
| } |
| |
| void AssistantManagerServiceImpl::UpdateInternalOptions( |
| assistant_client::AssistantManagerInternal* assistant_manager_internal) { |
| // Build internal options |
| auto* internal_options = |
| assistant_manager_internal->CreateDefaultInternalOptions(); |
| SetAssistantOptions(internal_options, assistant_state()->locale().value(), |
| spoken_feedback_enabled_); |
| |
| internal_options->SetClientControlEnabled( |
| assistant::features::IsRoutinesEnabled()); |
| |
| if (is_signed_out_mode_) { |
| internal_options->SetUserCredentialMode( |
| assistant_client::InternalOptions::UserCredentialMode::SIGNED_OUT); |
| } |
| |
| internal_options->EnableRequireVoiceMatchVerification(); |
| |
| assistant_manager_internal->SetOptions(*internal_options, [](bool success) { |
| DVLOG(2) << "set options: " << success; |
| }); |
| } |
| |
| void AssistantManagerServiceImpl::MediaSessionChanged( |
| const base::Optional<base::UnguessableToken>& request_id) { |
| if (request_id.has_value()) |
| media_session_audio_focus_id_ = std::move(request_id.value()); |
| } |
| |
| void AssistantManagerServiceImpl::MediaSessionInfoChanged( |
| media_session::mojom::MediaSessionInfoPtr info) { |
| media_session_info_ptr_ = std::move(info); |
| UpdateMediaState(); |
| } |
| |
| void AssistantManagerServiceImpl::MediaSessionMetadataChanged( |
| const base::Optional<media_session::MediaMetadata>& metadata) { |
| media_metadata_ = std::move(metadata); |
| UpdateMediaState(); |
| } |
| |
| void AssistantManagerServiceImpl::OnConversationTurnStartedOnMainThread( |
| const assistant_client::ConversationTurnMetadata& metadata) { |
| platform_api_->OnConversationTurnStarted(); |
| |
| // Retrieve the cached interaction metadata associated with this conversation |
| // turn or construct a new instance if there's no match in the cache. |
| mojom::AssistantInteractionMetadataPtr metadata_ptr; |
| auto it = pending_interactions_.find(metadata.id); |
| if (it != pending_interactions_.end()) { |
| metadata_ptr = std::move(it->second); |
| pending_interactions_.erase(it); |
| } else { |
| metadata_ptr = mojom::AssistantInteractionMetadata::New(); |
| metadata_ptr->type = metadata.is_mic_open |
| ? mojom::AssistantInteractionType::kVoice |
| : mojom::AssistantInteractionType::kText; |
| } |
| |
| for (auto& it : interaction_subscribers_) |
| it->OnInteractionStarted(metadata_ptr->Clone()); |
| } |
| |
| void AssistantManagerServiceImpl::OnConversationTurnFinishedOnMainThread( |
| assistant_client::ConversationStateListener::Resolution resolution) { |
| // TODO(updowndota): Find a better way to handle the edge cases. |
| if (resolution != Resolution::NORMAL_WITH_FOLLOW_ON && |
| resolution != Resolution::CANCELLED && |
| resolution != Resolution::BARGE_IN) { |
| platform_api_->SetMicState(false); |
| } |
| |
| platform_api_->OnConversationTurnFinished(); |
| |
| switch (resolution) { |
| // Interaction ended normally. |
| case Resolution::NORMAL: |
| case Resolution::NORMAL_WITH_FOLLOW_ON: |
| case Resolution::NO_RESPONSE: |
| for (auto& it : interaction_subscribers_) { |
| it->OnInteractionFinished( |
| mojom::AssistantInteractionResolution::kNormal); |
| } |
| |
| RecordQueryResponseTypeUMA(); |
| break; |
| // Interaction ended due to interruption. |
| case Resolution::BARGE_IN: |
| case Resolution::CANCELLED: |
| for (auto& it : interaction_subscribers_) { |
| it->OnInteractionFinished( |
| mojom::AssistantInteractionResolution::kInterruption); |
| } |
| |
| if (receive_inline_response_ || receive_modify_settings_proto_response_ || |
| !receive_url_response_.empty()) { |
| RecordQueryResponseTypeUMA(); |
| } |
| break; |
| // Interaction ended due to mic timeout. |
| case Resolution::TIMEOUT: |
| for (auto& it : interaction_subscribers_) { |
| it->OnInteractionFinished( |
| mojom::AssistantInteractionResolution::kMicTimeout); |
| } |
| break; |
| // Interaction ended due to error. |
| case Resolution::COMMUNICATION_ERROR: |
| for (auto& it : interaction_subscribers_) { |
| it->OnInteractionFinished( |
| mojom::AssistantInteractionResolution::kError); |
| } |
| break; |
| // Interaction ended because the device was not selected to produce a |
| // response. This occurs due to multi-device hotword loss. |
| case Resolution::DEVICE_NOT_SELECTED: |
| for (auto& it : interaction_subscribers_) { |
| it->OnInteractionFinished( |
| mojom::AssistantInteractionResolution::kMultiDeviceHotwordLoss); |
| } |
| break; |
| } |
| } |
| |
| void AssistantManagerServiceImpl::OnShowHtmlOnMainThread( |
| const std::string& html, |
| const std::string& fallback) { |
| receive_inline_response_ = true; |
| |
| for (auto& it : interaction_subscribers_) |
| it->OnHtmlResponse(html, fallback); |
| } |
| |
| void AssistantManagerServiceImpl::OnShowSuggestionsOnMainThread( |
| const std::vector<mojom::AssistantSuggestionPtr>& suggestions) { |
| for (auto& it : interaction_subscribers_) |
| it->OnSuggestionsResponse(mojo::Clone(suggestions)); |
| } |
| |
| void AssistantManagerServiceImpl::OnShowTextOnMainThread( |
| const std::string& text) { |
| receive_inline_response_ = true; |
| |
| for (auto& it : interaction_subscribers_) |
| it->OnTextResponse(text); |
| } |
| |
| void AssistantManagerServiceImpl::OnOpenUrlOnMainThread(const std::string& url, |
| bool in_background) { |
| receive_url_response_ = url; |
| const GURL gurl = GURL(url); |
| |
| for (auto& it : interaction_subscribers_) |
| it->OnOpenUrlResponse(gurl, in_background); |
| } |
| |
| void AssistantManagerServiceImpl::OnPlaybackStateChange( |
| const MediaStatus& status) { |
| if (media_session_) |
| media_session_->NotifyMediaSessionMetadataChanged(status); |
| } |
| |
| void AssistantManagerServiceImpl::OnShowNotificationOnMainThread( |
| const mojom::AssistantNotificationPtr& notification) { |
| assistant_notification_controller()->AddOrUpdateNotification( |
| notification.Clone()); |
| } |
| |
| void AssistantManagerServiceImpl::OnNotificationRemovedOnMainThread( |
| const std::string& grouping_key) { |
| if (grouping_key.empty()) { |
| assistant_notification_controller()->RemoveAllNotifications( |
| /*from_server=*/true); |
| } else { |
| assistant_notification_controller()->RemoveNotificationByGroupingKey( |
| grouping_key, /*from_server=*/ |
| true); |
| } |
| } |
| |
| void AssistantManagerServiceImpl::OnCommunicationErrorOnMainThread( |
| int error_code) { |
| CommunicationErrorType type = |
| CommunicationErrorTypeFromLibassistantErrorCode(error_code); |
| |
| for (auto& observer : error_observers_) |
| observer.OnCommunicationError(type); |
| } |
| |
| void AssistantManagerServiceImpl::OnRecognitionStateChangedOnMainThread( |
| assistant_client::ConversationStateListener::RecognitionState state, |
| const assistant_client::ConversationStateListener::RecognitionResult& |
| recognition_result) { |
| switch (state) { |
| case assistant_client::ConversationStateListener::RecognitionState::STARTED: |
| for (auto& it : interaction_subscribers_) |
| it->OnSpeechRecognitionStarted(); |
| break; |
| case assistant_client::ConversationStateListener::RecognitionState:: |
| INTERMEDIATE_RESULT: |
| for (auto& it : interaction_subscribers_) { |
| it->OnSpeechRecognitionIntermediateResult( |
| recognition_result.high_confidence_text, |
| recognition_result.low_confidence_text); |
| } |
| break; |
| case assistant_client::ConversationStateListener::RecognitionState:: |
| END_OF_UTTERANCE: |
| for (auto& it : interaction_subscribers_) |
| it->OnSpeechRecognitionEndOfUtterance(); |
| break; |
| case assistant_client::ConversationStateListener::RecognitionState:: |
| FINAL_RESULT: |
| for (auto& it : interaction_subscribers_) { |
| it->OnSpeechRecognitionFinalResult( |
| recognition_result.recognized_speech); |
| } |
| break; |
| } |
| } |
| |
| void AssistantManagerServiceImpl::OnRespondingStartedOnMainThread( |
| bool is_error_response) { |
| for (auto& it : interaction_subscribers_) |
| it->OnTtsStarted(is_error_response); |
| } |
| |
| void AssistantManagerServiceImpl::OnSpeechLevelUpdatedOnMainThread( |
| const float speech_level) { |
| for (auto& it : interaction_subscribers_) |
| it->OnSpeechLevelUpdated(speech_level); |
| } |
| |
| void AssistantManagerServiceImpl::OnAlarmTimerStateChangedOnMainThread() { |
| // Currently, we only handle ringing events here. After some AlarmTimerManager |
| // API improvement, we will be handling other alarm/timer events. |
| auto* alarm_timer_manager = |
| assistant_manager_internal_->GetAlarmTimerManager(); |
| // TODO(llin): Use GetAllEvents after the AlarmTimerManager API improvement is |
| // ready (b/128701326). |
| const assistant_client::AlarmTimerEvent& ringing_event = |
| alarm_timer_manager->GetRingingEvent(); |
| |
| switch (ringing_event.type) { |
| case assistant_client::AlarmTimerEvent::NONE: |
| assistant_alarm_timer_controller()->OnAlarmTimerStateChanged(nullptr); |
| break; |
| case assistant_client::AlarmTimerEvent::TIMER: { |
| ash::mojom::AssistantAlarmTimerEventPtr alarm_timer_event_ptr = |
| ash::mojom::AssistantAlarmTimerEvent::New(); |
| alarm_timer_event_ptr->type = |
| ash::mojom::AssistantAlarmTimerEventType::kTimer; |
| |
| if (ringing_event.type == assistant_client::AlarmTimerEvent::TIMER) { |
| alarm_timer_event_ptr->data = ash::mojom::AlarmTimerData::New(); |
| ash::mojom::AssistantTimerPtr timer_data_ptr = |
| ash::mojom::AssistantTimer::New(); |
| timer_data_ptr->state = GetTimerState(ringing_event.timer_data.state); |
| timer_data_ptr->timer_id = ringing_event.timer_data.timer_id; |
| alarm_timer_event_ptr->data->set_timer_data(std::move(timer_data_ptr)); |
| } |
| |
| assistant_alarm_timer_controller()->OnAlarmTimerStateChanged( |
| std::move(alarm_timer_event_ptr)); |
| break; |
| } |
| case assistant_client::AlarmTimerEvent::ALARM: |
| // TODO(llin): Handle alarm. |
| NOTREACHED(); |
| break; |
| } |
| } |
| |
| void AssistantManagerServiceImpl::CacheScreenContext( |
| CacheScreenContextCallback callback) { |
| if (!IsScreenContextAllowed(assistant_state())) { |
| std::move(callback).Run(); |
| return; |
| } |
| |
| // Our callback should be run only after both view hierarchy and screenshot |
| // data have been cached from their respective providers. |
| auto on_done = base::BarrierClosure(2, std::move(callback)); |
| |
| client_->RequestAssistantStructure( |
| base::BindOnce(&AssistantManagerServiceImpl::CacheAssistantStructure, |
| weak_factory_.GetWeakPtr(), on_done)); |
| |
| assistant_screen_context_controller()->RequestScreenshot( |
| gfx::Rect(), |
| base::BindOnce(&AssistantManagerServiceImpl::CacheAssistantScreenshot, |
| weak_factory_.GetWeakPtr(), on_done)); |
| } |
| |
| void AssistantManagerServiceImpl::ClearScreenContextCache() { |
| assistant_extra_.reset(); |
| assistant_tree_.reset(); |
| assistant_screenshot_.clear(); |
| is_first_client_discourse_context_query_ = true; |
| } |
| |
| void AssistantManagerServiceImpl::OnAccessibilityStatusChanged( |
| bool spoken_feedback_enabled) { |
| if (spoken_feedback_enabled_ == spoken_feedback_enabled) |
| return; |
| |
| spoken_feedback_enabled_ = spoken_feedback_enabled; |
| |
| // When |spoken_feedback_enabled_| changes we need to update our internal |
| // options to turn on/off A11Y features in LibAssistant. |
| if (assistant_manager_internal_) |
| UpdateInternalOptions(assistant_manager_internal_); |
| } |
| |
| void AssistantManagerServiceImpl::StopAlarmTimerRinging() { |
| if (!assistant_manager_internal_) |
| return; |
| |
| assistant_manager_internal_->GetAlarmTimerManager()->StopRinging(); |
| } |
| |
| void AssistantManagerServiceImpl::CreateTimer(base::TimeDelta duration) { |
| if (!assistant_manager_internal_) |
| return; |
| |
| assistant_manager_internal_->GetAlarmTimerManager()->CreateTimer( |
| duration.InSeconds(), /*label=*/std::string()); |
| } |
| |
| void AssistantManagerServiceImpl::CacheAssistantStructure( |
| base::OnceClosure on_done, |
| ax::mojom::AssistantExtraPtr assistant_extra, |
| std::unique_ptr<ui::AssistantTree> assistant_tree) { |
| assistant_extra_ = std::move(assistant_extra); |
| assistant_tree_ = std::move(assistant_tree); |
| std::move(on_done).Run(); |
| } |
| |
| void AssistantManagerServiceImpl::CacheAssistantScreenshot( |
| base::OnceClosure on_done, |
| const std::vector<uint8_t>& assistant_screenshot) { |
| assistant_screenshot_ = assistant_screenshot; |
| std::move(on_done).Run(); |
| } |
| |
| void AssistantManagerServiceImpl::SendScreenContextRequest( |
| ax::mojom::AssistantExtra* assistant_extra, |
| ui::AssistantTree* assistant_tree, |
| const std::vector<uint8_t>& assistant_screenshot) { |
| if (assistant::features::IsScreenContextQueryEnabled()) { |
| assistant_client::VoicelessOptions options; |
| options.is_user_initiated = true; |
| |
| assistant_manager_internal_->SendTextQueryWithClientDiscourseContext( |
| kScreenContextQuery, |
| CreateContextProto( |
| AssistantBundle{assistant_extra_.get(), assistant_tree_.get()}, |
| assistant_screenshot), |
| options); |
| return; |
| } |
| |
| std::vector<std::string> context_protos; |
| |
| // Screen context can have the assistant_extra and assistant_tree set to |
| // nullptr. This happens in the case where the screen context is coming from |
| // the metalayer. For this scenario, we don't create a context proto for the |
| // AssistantBundle that consists of the assistant_extra and assistant_tree. |
| if (assistant_extra && assistant_tree) { |
| // Note: the value of is_first_query for screen context query is a no-op |
| // because it is not used for metalayer and "What's on my screen" queries. |
| context_protos.emplace_back( |
| CreateContextProto(AssistantBundle{assistant_extra, assistant_tree}, |
| /*is_first_query=*/true)); |
| } |
| |
| // Note: the value of is_first_query for screen context query is a no-op. |
| context_protos.emplace_back(CreateContextProto(assistant_screenshot, |
| /*is_first_query=*/true)); |
| assistant_manager_internal_->SendScreenContextRequest(context_protos); |
| } |
| |
| std::string AssistantManagerServiceImpl::GetLastSearchSource() { |
| base::AutoLock lock(last_search_source_lock_); |
| auto search_source = last_search_source_; |
| last_search_source_ = std::string(); |
| return search_source; |
| } |
| |
| void AssistantManagerServiceImpl::FillServerExperimentIds( |
| std::vector<std::string>* server_experiment_ids) { |
| if (base::FeatureList::IsEnabled(kChromeOSAssistantDogfood)) { |
| server_experiment_ids->emplace_back(kServersideDogfoodExperimentId); |
| } |
| |
| if (base::FeatureList::IsEnabled(assistant::features::kAssistantAppSupport)) |
| server_experiment_ids->emplace_back(kServersideOpenAppExperimentId); |
| } |
| |
| void AssistantManagerServiceImpl::RecordQueryResponseTypeUMA() { |
| auto response_type = AssistantQueryResponseType::kUnspecified; |
| |
| if (receive_modify_settings_proto_response_) { |
| response_type = AssistantQueryResponseType::kDeviceAction; |
| } else if (!receive_url_response_.empty()) { |
| if (receive_url_response_.find("www.google.com/search?") != |
| std::string::npos) { |
| response_type = AssistantQueryResponseType::kSearchFallback; |
| } else { |
| response_type = AssistantQueryResponseType::kTargetedAction; |
| } |
| } else if (receive_inline_response_) { |
| response_type = AssistantQueryResponseType::kInlineElement; |
| } |
| |
| UMA_HISTOGRAM_ENUMERATION("Assistant.QueryResponseType", response_type); |
| |
| // Reset the flags. |
| receive_inline_response_ = false; |
| receive_modify_settings_proto_response_ = false; |
| receive_url_response_.clear(); |
| } |
| |
| void AssistantManagerServiceImpl::SendAssistantFeedback( |
| mojom::AssistantFeedbackPtr assistant_feedback) { |
| const std::string interaction = CreateSendFeedbackInteraction( |
| assistant_feedback->assistant_debug_info_allowed, |
| assistant_feedback->description, assistant_feedback->screenshot_png); |
| assistant_client::VoicelessOptions voiceless_options; |
| |
| voiceless_options.is_user_initiated = false; |
| |
| assistant_manager_internal_->SendVoicelessInteraction( |
| interaction, "send feedback with details", voiceless_options, |
| [](auto) {}); |
| } |
| |
| void AssistantManagerServiceImpl::UpdateMediaState() { |
| if (media_session_info_ptr_) { |
| if (media_session_info_ptr_->is_sensitive) { |
| // Do not update media state if the session is considered to be sensitive |
| // (off the record profile). |
| return; |
| } |
| |
| if (media_session_info_ptr_->state == |
| media_session::mojom::MediaSessionInfo::SessionState::kSuspended && |
| media_session_info_ptr_->playback_state == |
| media_session::mojom::MediaPlaybackState::kPlaying) { |
| // It is an intermediate state caused by some providers override the |
| // playback state. We considered it as invalid and skip reporting the |
| // state. |
| return; |
| } |
| } |
| |
| // MediaSession Integrated providers (include the libassistant internal |
| // media provider) will trigger media state change event. Only update the |
| // external media status if the state changes is triggered by external |
| // providers. |
| if (media_session_ && media_session_->internal_audio_focus_id() == |
| media_session_audio_focus_id_) { |
| return; |
| } |
| |
| MediaStatus media_status; |
| |
| // Set media metadata. |
| if (media_metadata_.has_value()) { |
| media_status.metadata.title = |
| base::UTF16ToUTF8(media_metadata_.value().title); |
| } |
| |
| // Set playback state. |
| media_status.playback_state = MediaStatus::IDLE; |
| if (media_session_info_ptr_ && |
| media_session_info_ptr_->state != |
| media_session::mojom::MediaSessionInfo::SessionState::kInactive) { |
| switch (media_session_info_ptr_->playback_state) { |
| case media_session::mojom::MediaPlaybackState::kPlaying: |
| media_status.playback_state = MediaStatus::PLAYING; |
| break; |
| case media_session::mojom::MediaPlaybackState::kPaused: |
| media_status.playback_state = MediaStatus::PAUSED; |
| break; |
| } |
| } |
| |
| auto* media_manager = assistant_manager_->GetMediaManager(); |
| if (media_manager) |
| media_manager->SetExternalPlaybackState(media_status); |
| } |
| |
| ash::mojom::AssistantAlarmTimerController* |
| AssistantManagerServiceImpl::assistant_alarm_timer_controller() { |
| return context_->assistant_alarm_timer_controller(); |
| } |
| |
| ash::mojom::AssistantNotificationController* |
| AssistantManagerServiceImpl::assistant_notification_controller() { |
| return context_->assistant_notification_controller(); |
| } |
| |
| ash::mojom::AssistantScreenContextController* |
| AssistantManagerServiceImpl::assistant_screen_context_controller() { |
| return context_->assistant_screen_context_controller(); |
| } |
| |
| ash::AssistantStateBase* AssistantManagerServiceImpl::assistant_state() { |
| return context_->assistant_state(); |
| } |
| |
| mojom::DeviceActions* AssistantManagerServiceImpl::device_actions() { |
| return context_->device_actions(); |
| } |
| |
| scoped_refptr<base::SequencedTaskRunner> |
| AssistantManagerServiceImpl::main_task_runner() { |
| return context_->main_task_runner(); |
| } |
| |
| bool AssistantManagerServiceImpl::HasStartFinished() const { |
| return state_ == State::RUNNING; |
| } |
| |
| void AssistantManagerServiceImpl::SetStartFinished() { |
| state_ = State::RUNNING; |
| } |
| |
| } // namespace assistant |
| } // namespace chromeos |