| // 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 "ash/assistant/assistant_suggestions_controller_impl.h" |
| |
| #include <algorithm> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "ash/assistant/model/assistant_ui_model.h" |
| #include "ash/assistant/util/assistant_util.h" |
| #include "ash/assistant/util/deep_link_util.h" |
| #include "ash/assistant/util/resource_util.h" |
| #include "ash/public/cpp/assistant/controller/assistant_ui_controller.h" |
| #include "ash/public/cpp/assistant/conversation_starter.h" |
| #include "ash/public/cpp/assistant/conversation_starters_client.h" |
| #include "ash/shell.h" |
| #include "ash/strings/grit/ash_strings.h" |
| #include "base/rand_util.h" |
| #include "base/stl_util.h" |
| #include "base/unguessable_token.h" |
| #include "chromeos/services/assistant/public/cpp/assistant_prefs.h" |
| #include "chromeos/services/assistant/public/cpp/assistant_service.h" |
| #include "chromeos/services/assistant/public/cpp/features.h" |
| #include "ui/base/l10n/l10n_util.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| using chromeos::assistant::AssistantSuggestion; |
| using chromeos::assistant::AssistantSuggestionType; |
| using chromeos::assistant::features::IsBetterOnboardingEnabled; |
| using chromeos::assistant::features::IsConversationStartersV2Enabled; |
| using chromeos::assistant::prefs::AssistantOnboardingMode; |
| |
| // Conversation starters ------------------------------------------------------- |
| |
| constexpr int kMaxNumOfConversationStarters = 3; |
| |
| bool IsAllowed(const ConversationStarter& conversation_starter) { |
| using Permission = ConversationStarter::Permission; |
| |
| if (conversation_starter.RequiresPermission(Permission::kUnknown)) |
| return false; |
| |
| if (conversation_starter.RequiresPermission(Permission::kRelatedInfo) && |
| !AssistantState::Get()->context_enabled().value_or(false)) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| AssistantSuggestion ToAssistantSuggestion( |
| const ConversationStarter& conversation_starter) { |
| AssistantSuggestion suggestion; |
| suggestion.id = base::UnguessableToken::Create(); |
| suggestion.type = AssistantSuggestionType::kConversationStarter; |
| suggestion.text = conversation_starter.label(); |
| |
| if (conversation_starter.action_url().has_value()) |
| suggestion.action_url = conversation_starter.action_url().value(); |
| |
| if (conversation_starter.icon_url().has_value()) |
| suggestion.icon_url = conversation_starter.icon_url().value(); |
| |
| return suggestion; |
| } |
| |
| } // namespace |
| |
| // AssistantSuggestionsControllerImpl ------------------------------------------ |
| |
| AssistantSuggestionsControllerImpl::AssistantSuggestionsControllerImpl() { |
| // In conversation starters V2, we only update conversation starters when the |
| // Assistant UI is becoming visible so as to maximize freshness. |
| if (!IsConversationStartersV2Enabled()) |
| UpdateConversationStarters(); |
| |
| assistant_controller_observer_.Add(AssistantController::Get()); |
| } |
| |
| AssistantSuggestionsControllerImpl::~AssistantSuggestionsControllerImpl() = |
| default; |
| |
| const AssistantSuggestionsModel* AssistantSuggestionsControllerImpl::GetModel() |
| const { |
| return &model_; |
| } |
| |
| void AssistantSuggestionsControllerImpl::OnAssistantControllerConstructed() { |
| AssistantUiController::Get()->GetModel()->AddObserver(this); |
| AssistantState::Get()->AddObserver(this); |
| } |
| |
| void AssistantSuggestionsControllerImpl::OnAssistantControllerDestroying() { |
| AssistantState::Get()->RemoveObserver(this); |
| AssistantUiController::Get()->GetModel()->RemoveObserver(this); |
| } |
| |
| void AssistantSuggestionsControllerImpl::OnUiVisibilityChanged( |
| AssistantVisibility new_visibility, |
| AssistantVisibility old_visibility, |
| base::Optional<AssistantEntryPoint> entry_point, |
| base::Optional<AssistantExitPoint> exit_point) { |
| if (IsConversationStartersV2Enabled()) { |
| // When Assistant is starting a session, we update our cache of conversation |
| // starters so that they are as fresh as possible. Note that we may need to |
| // modify this logic later if latency becomes a concern. |
| if (assistant::util::IsStartingSession(new_visibility, old_visibility)) { |
| UpdateConversationStarters(); |
| return; |
| } |
| // When Assistant is finishing a session, we clear our cache of conversation |
| // starters so that, when the next session begins, we won't show stale |
| // conversation starters while we fetch fresh ones. |
| if (assistant::util::IsFinishingSession(new_visibility)) { |
| conversation_starters_weak_factory_.InvalidateWeakPtrs(); |
| model_.SetConversationStarters({}); |
| } |
| return; |
| } |
| |
| DCHECK(!IsConversationStartersV2Enabled()); |
| |
| // When Assistant is finishing a session, we update our cache of conversation |
| // starters so that they're fresh for the next launch. |
| if (assistant::util::IsFinishingSession(new_visibility)) |
| UpdateConversationStarters(); |
| } |
| |
| void AssistantSuggestionsControllerImpl::OnAssistantContextEnabled( |
| bool enabled) { |
| // We currently assume that the context setting is not being modified while |
| // Assistant UI is visible. |
| DCHECK_NE(AssistantVisibility::kVisible, |
| AssistantUiController::Get()->GetModel()->visibility()); |
| |
| // In conversation starters V2, we only update conversation starters when |
| // Assistant UI is becoming visible so as to maximize freshness. |
| if (IsConversationStartersV2Enabled()) |
| return; |
| |
| UpdateConversationStarters(); |
| } |
| |
| void AssistantSuggestionsControllerImpl::OnAssistantOnboardingModeChanged( |
| AssistantOnboardingMode onboarding_mode) { |
| // Onboarding suggestions are only applicable if the feature is enabled. |
| if (IsBetterOnboardingEnabled()) |
| UpdateOnboardingSuggestions(); |
| } |
| |
| void AssistantSuggestionsControllerImpl::UpdateConversationStarters() { |
| // If conversation starters V2 is enabled, we'll fetch a fresh set of |
| // conversation starters from the server. |
| if (IsConversationStartersV2Enabled()) { |
| FetchConversationStarters(); |
| return; |
| } |
| // Otherwise we'll use a locally provided set of conversation starters. |
| ProvideConversationStarters(); |
| } |
| |
| void AssistantSuggestionsControllerImpl::FetchConversationStarters() { |
| DCHECK(IsConversationStartersV2Enabled()); |
| |
| // Invalidate any requests that are already in flight. |
| conversation_starters_weak_factory_.InvalidateWeakPtrs(); |
| |
| // Fetch a fresh set of conversation starters from the server (via the |
| // dedicated ConversationStartersClient). |
| ConversationStartersClient::Get()->FetchConversationStarters(base::BindOnce( |
| [](const base::WeakPtr<AssistantSuggestionsControllerImpl>& self, |
| std::vector<ConversationStarter>&& conversation_starters) { |
| if (!self) |
| return; |
| |
| // Remove any conversation starters which we determine to not be allowed |
| // based on the required permissions that they specify. Note that this |
| // no-ops if the collection is empty. |
| base::EraseIf(conversation_starters, |
| [](const ConversationStarter& conversation_starter) { |
| return !IsAllowed(conversation_starter); |
| }); |
| |
| // When the server doesn't respond with any conversation starters that |
| // we can present, we'll fallback to the locally provided set. |
| if (conversation_starters.empty()) { |
| self->ProvideConversationStarters(); |
| return; |
| } |
| |
| // The number of conversation starters should not exceed our maximum. |
| while (conversation_starters.size() > kMaxNumOfConversationStarters) |
| conversation_starters.pop_back(); |
| |
| // We need to transform our conversation starters into the type that is |
| // understood by the suggestions model... |
| std::vector<AssistantSuggestion> suggestions; |
| std::transform(conversation_starters.begin(), |
| conversation_starters.end(), |
| std::back_inserter(suggestions), ToAssistantSuggestion); |
| |
| // ...and we update our cache. |
| self->model_.SetConversationStarters(std::move(suggestions)); |
| }, |
| conversation_starters_weak_factory_.GetWeakPtr())); |
| } |
| |
| void AssistantSuggestionsControllerImpl::ProvideConversationStarters() { |
| std::vector<AssistantSuggestion> conversation_starters; |
| |
| // Adds a conversation starter for the given |message_id| and |action_url|. |
| auto AddConversationStarter = [&conversation_starters]( |
| int message_id, GURL action_url = GURL()) { |
| AssistantSuggestion starter; |
| starter.id = base::UnguessableToken::Create(); |
| starter.type = AssistantSuggestionType::kConversationStarter; |
| starter.text = l10n_util::GetStringUTF8(message_id); |
| starter.action_url = action_url; |
| conversation_starters.push_back(std::move(starter)); |
| }; |
| |
| // Always show the "What can you do?" conversation starter. |
| AddConversationStarter(IDS_ASH_ASSISTANT_CHIP_WHAT_CAN_YOU_DO); |
| |
| // If enabled, always show the "What's on my screen?" conversation starter. |
| if (AssistantState::Get()->context_enabled().value_or(false)) { |
| AddConversationStarter(IDS_ASH_ASSISTANT_CHIP_WHATS_ON_MY_SCREEN, |
| assistant::util::CreateWhatsOnMyScreenDeepLink()); |
| } |
| |
| // The rest of the conversation starters will be shuffled... |
| std::vector<int> shuffled_message_ids; |
| |
| shuffled_message_ids.push_back(IDS_ASH_ASSISTANT_CHIP_IM_BORED); |
| shuffled_message_ids.push_back(IDS_ASH_ASSISTANT_CHIP_OPEN_FILES); |
| shuffled_message_ids.push_back(IDS_ASH_ASSISTANT_CHIP_PLAY_MUSIC); |
| shuffled_message_ids.push_back(IDS_ASH_ASSISTANT_CHIP_SEND_AN_EMAIL); |
| shuffled_message_ids.push_back(IDS_ASH_ASSISTANT_CHIP_SET_A_REMINDER); |
| shuffled_message_ids.push_back(IDS_ASH_ASSISTANT_CHIP_WHATS_ON_MY_CALENDAR); |
| shuffled_message_ids.push_back(IDS_ASH_ASSISTANT_CHIP_WHATS_THE_WEATHER); |
| |
| base::RandomShuffle(shuffled_message_ids.begin(), shuffled_message_ids.end()); |
| |
| // ...and added until we have no more than |kMaxNumOfConversationStarters|. |
| for (int i = 0; |
| conversation_starters.size() < kMaxNumOfConversationStarters && |
| i < static_cast<int>(shuffled_message_ids.size()); |
| ++i) { |
| AddConversationStarter(shuffled_message_ids[i]); |
| } |
| |
| model_.SetConversationStarters(std::move(conversation_starters)); |
| } |
| |
| void AssistantSuggestionsControllerImpl::UpdateOnboardingSuggestions() { |
| DCHECK(IsBetterOnboardingEnabled()); |
| |
| auto CreateIconResourceLink = [](int message_id) { |
| switch (message_id) { |
| case IDS_ASH_ASSISTANT_ONBOARDING_SUGGESTION_CONVERSION: |
| return assistant::util::CreateIconResourceLink( |
| assistant::util::IconName::kConversionPath); |
| case IDS_ASH_ASSISTANT_ONBOARDING_SUGGESTION_KNOWLEDGE: |
| return assistant::util::CreateIconResourceLink( |
| assistant::util::IconName::kPersonPinCircle); |
| case IDS_ASH_ASSISTANT_ONBOARDING_SUGGESTION_KNOWLEDGE_EDU: |
| return assistant::util::CreateIconResourceLink( |
| assistant::util::IconName::kStraighten); |
| case IDS_ASH_ASSISTANT_ONBOARDING_SUGGESTION_LANGUAGE: |
| return assistant::util::CreateIconResourceLink( |
| assistant::util::IconName::kTranslate); |
| case IDS_ASH_ASSISTANT_ONBOARDING_SUGGESTION_MATH: |
| return assistant::util::CreateIconResourceLink( |
| assistant::util::IconName::kCalculate); |
| case IDS_ASH_ASSISTANT_ONBOARDING_SUGGESTION_PERSONALITY: |
| return assistant::util::CreateIconResourceLink( |
| assistant::util::IconName::kSentimentVerySatisfied); |
| case IDS_ASH_ASSISTANT_ONBOARDING_SUGGESTION_PRODUCTIVITY: |
| return assistant::util::CreateIconResourceLink( |
| assistant::util::IconName::kTimer); |
| case IDS_ASH_ASSISTANT_ONBOARDING_SUGGESTION_TECHNICAL: |
| return assistant::util::CreateIconResourceLink( |
| assistant::util::IconName::kScreenshot); |
| default: |
| NOTREACHED(); |
| return GURL(); |
| } |
| }; |
| |
| std::vector<AssistantSuggestion> onboarding_suggestions; |
| |
| using chromeos::assistant::AssistantBetterOnboardingType; |
| auto AddSuggestion = [&CreateIconResourceLink, &onboarding_suggestions]( |
| int message_id, AssistantBetterOnboardingType type) { |
| onboarding_suggestions.emplace_back(); |
| auto& suggestion = onboarding_suggestions.back(); |
| suggestion.id = base::UnguessableToken::Create(); |
| suggestion.type = AssistantSuggestionType::kBetterOnboarding; |
| suggestion.better_onboarding_type = type; |
| suggestion.text = l10n_util::GetStringUTF8(message_id); |
| suggestion.icon_url = CreateIconResourceLink(message_id); |
| suggestion.action_url = GURL(); |
| }; |
| |
| switch (AssistantState::Get()->onboarding_mode().value_or( |
| AssistantOnboardingMode::kDefault)) { |
| case AssistantOnboardingMode::kEducation: |
| AddSuggestion(IDS_ASH_ASSISTANT_ONBOARDING_SUGGESTION_MATH, |
| AssistantBetterOnboardingType::kMath); |
| AddSuggestion(IDS_ASH_ASSISTANT_ONBOARDING_SUGGESTION_KNOWLEDGE_EDU, |
| AssistantBetterOnboardingType::kKnowledgeEdu); |
| break; |
| case AssistantOnboardingMode::kDefault: |
| AddSuggestion(IDS_ASH_ASSISTANT_ONBOARDING_SUGGESTION_CONVERSION, |
| AssistantBetterOnboardingType::kConversion); |
| AddSuggestion(IDS_ASH_ASSISTANT_ONBOARDING_SUGGESTION_KNOWLEDGE, |
| AssistantBetterOnboardingType::kKnowledge); |
| break; |
| } |
| |
| AddSuggestion(IDS_ASH_ASSISTANT_ONBOARDING_SUGGESTION_PRODUCTIVITY, |
| AssistantBetterOnboardingType::kProductivity); |
| AddSuggestion(IDS_ASH_ASSISTANT_ONBOARDING_SUGGESTION_PERSONALITY, |
| AssistantBetterOnboardingType::kPersonality); |
| AddSuggestion(IDS_ASH_ASSISTANT_ONBOARDING_SUGGESTION_LANGUAGE, |
| AssistantBetterOnboardingType::kLanguage); |
| AddSuggestion(IDS_ASH_ASSISTANT_ONBOARDING_SUGGESTION_TECHNICAL, |
| AssistantBetterOnboardingType::kTechnical); |
| |
| model_.SetOnboardingSuggestions(std::move(onboarding_suggestions)); |
| } |
| |
| } // namespace ash |