1 // Copyright 2018 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "ash/assistant/assistant_suggestions_controller_impl.h"
6
7 #include <algorithm>
8 #include <string>
9 #include <utility>
10 #include <vector>
11
12 #include "ash/assistant/model/assistant_ui_model.h"
13 #include "ash/assistant/util/assistant_util.h"
14 #include "ash/assistant/util/deep_link_util.h"
15 #include "ash/assistant/util/resource_util.h"
16 #include "ash/public/cpp/assistant/controller/assistant_ui_controller.h"
17 #include "ash/public/cpp/assistant/conversation_starter.h"
18 #include "ash/public/cpp/assistant/conversation_starters_client.h"
19 #include "ash/shell.h"
20 #include "ash/strings/grit/ash_strings.h"
21 #include "base/rand_util.h"
22 #include "base/stl_util.h"
23 #include "base/unguessable_token.h"
24 #include "chromeos/services/assistant/public/cpp/assistant_prefs.h"
25 #include "chromeos/services/assistant/public/cpp/assistant_service.h"
26 #include "chromeos/services/assistant/public/cpp/features.h"
27 #include "ui/base/l10n/l10n_util.h"
28
29 namespace ash {
30
31 namespace {
32
33 using chromeos::assistant::AssistantSuggestion;
34 using chromeos::assistant::AssistantSuggestionType;
35 using chromeos::assistant::features::IsBetterOnboardingEnabled;
36 using chromeos::assistant::features::IsConversationStartersV2Enabled;
37 using chromeos::assistant::prefs::AssistantOnboardingMode;
38
39 // Conversation starters -------------------------------------------------------
40
41 constexpr int kMaxNumOfConversationStarters = 3;
42
IsAllowed(const ConversationStarter & conversation_starter)43 bool IsAllowed(const ConversationStarter& conversation_starter) {
44 using Permission = ConversationStarter::Permission;
45
46 if (conversation_starter.RequiresPermission(Permission::kUnknown))
47 return false;
48
49 if (conversation_starter.RequiresPermission(Permission::kRelatedInfo) &&
50 !AssistantState::Get()->context_enabled().value_or(false)) {
51 return false;
52 }
53
54 return true;
55 }
56
ToAssistantSuggestion(const ConversationStarter & conversation_starter)57 AssistantSuggestion ToAssistantSuggestion(
58 const ConversationStarter& conversation_starter) {
59 AssistantSuggestion suggestion;
60 suggestion.id = base::UnguessableToken::Create();
61 suggestion.type = AssistantSuggestionType::kConversationStarter;
62 suggestion.text = conversation_starter.label();
63
64 if (conversation_starter.action_url().has_value())
65 suggestion.action_url = conversation_starter.action_url().value();
66
67 if (conversation_starter.icon_url().has_value())
68 suggestion.icon_url = conversation_starter.icon_url().value();
69
70 return suggestion;
71 }
72
73 } // namespace
74
75 // AssistantSuggestionsControllerImpl ------------------------------------------
76
AssistantSuggestionsControllerImpl()77 AssistantSuggestionsControllerImpl::AssistantSuggestionsControllerImpl() {
78 // In conversation starters V2, we only update conversation starters when the
79 // Assistant UI is becoming visible so as to maximize freshness.
80 if (!IsConversationStartersV2Enabled())
81 UpdateConversationStarters();
82
83 assistant_controller_observer_.Add(AssistantController::Get());
84 }
85
86 AssistantSuggestionsControllerImpl::~AssistantSuggestionsControllerImpl() =
87 default;
88
GetModel() const89 const AssistantSuggestionsModel* AssistantSuggestionsControllerImpl::GetModel()
90 const {
91 return &model_;
92 }
93
OnAssistantControllerConstructed()94 void AssistantSuggestionsControllerImpl::OnAssistantControllerConstructed() {
95 AssistantUiController::Get()->GetModel()->AddObserver(this);
96 AssistantState::Get()->AddObserver(this);
97 }
98
OnAssistantControllerDestroying()99 void AssistantSuggestionsControllerImpl::OnAssistantControllerDestroying() {
100 AssistantState::Get()->RemoveObserver(this);
101 AssistantUiController::Get()->GetModel()->RemoveObserver(this);
102 }
103
OnUiVisibilityChanged(AssistantVisibility new_visibility,AssistantVisibility old_visibility,base::Optional<AssistantEntryPoint> entry_point,base::Optional<AssistantExitPoint> exit_point)104 void AssistantSuggestionsControllerImpl::OnUiVisibilityChanged(
105 AssistantVisibility new_visibility,
106 AssistantVisibility old_visibility,
107 base::Optional<AssistantEntryPoint> entry_point,
108 base::Optional<AssistantExitPoint> exit_point) {
109 if (IsConversationStartersV2Enabled()) {
110 // When Assistant is starting a session, we update our cache of conversation
111 // starters so that they are as fresh as possible. Note that we may need to
112 // modify this logic later if latency becomes a concern.
113 if (assistant::util::IsStartingSession(new_visibility, old_visibility)) {
114 UpdateConversationStarters();
115 return;
116 }
117 // When Assistant is finishing a session, we clear our cache of conversation
118 // starters so that, when the next session begins, we won't show stale
119 // conversation starters while we fetch fresh ones.
120 if (assistant::util::IsFinishingSession(new_visibility)) {
121 conversation_starters_weak_factory_.InvalidateWeakPtrs();
122 model_.SetConversationStarters({});
123 }
124 return;
125 }
126
127 DCHECK(!IsConversationStartersV2Enabled());
128
129 // When Assistant is finishing a session, we update our cache of conversation
130 // starters so that they're fresh for the next launch.
131 if (assistant::util::IsFinishingSession(new_visibility))
132 UpdateConversationStarters();
133 }
134
OnAssistantContextEnabled(bool enabled)135 void AssistantSuggestionsControllerImpl::OnAssistantContextEnabled(
136 bool enabled) {
137 // We currently assume that the context setting is not being modified while
138 // Assistant UI is visible.
139 DCHECK_NE(AssistantVisibility::kVisible,
140 AssistantUiController::Get()->GetModel()->visibility());
141
142 // In conversation starters V2, we only update conversation starters when
143 // Assistant UI is becoming visible so as to maximize freshness.
144 if (IsConversationStartersV2Enabled())
145 return;
146
147 UpdateConversationStarters();
148 }
149
OnAssistantOnboardingModeChanged(AssistantOnboardingMode onboarding_mode)150 void AssistantSuggestionsControllerImpl::OnAssistantOnboardingModeChanged(
151 AssistantOnboardingMode onboarding_mode) {
152 // Onboarding suggestions are only applicable if the feature is enabled.
153 if (IsBetterOnboardingEnabled())
154 UpdateOnboardingSuggestions();
155 }
156
UpdateConversationStarters()157 void AssistantSuggestionsControllerImpl::UpdateConversationStarters() {
158 // If conversation starters V2 is enabled, we'll fetch a fresh set of
159 // conversation starters from the server.
160 if (IsConversationStartersV2Enabled()) {
161 FetchConversationStarters();
162 return;
163 }
164 // Otherwise we'll use a locally provided set of conversation starters.
165 ProvideConversationStarters();
166 }
167
FetchConversationStarters()168 void AssistantSuggestionsControllerImpl::FetchConversationStarters() {
169 DCHECK(IsConversationStartersV2Enabled());
170
171 // Invalidate any requests that are already in flight.
172 conversation_starters_weak_factory_.InvalidateWeakPtrs();
173
174 // Fetch a fresh set of conversation starters from the server (via the
175 // dedicated ConversationStartersClient).
176 ConversationStartersClient::Get()->FetchConversationStarters(base::BindOnce(
177 [](const base::WeakPtr<AssistantSuggestionsControllerImpl>& self,
178 std::vector<ConversationStarter>&& conversation_starters) {
179 if (!self)
180 return;
181
182 // Remove any conversation starters which we determine to not be allowed
183 // based on the required permissions that they specify. Note that this
184 // no-ops if the collection is empty.
185 base::EraseIf(conversation_starters,
186 [](const ConversationStarter& conversation_starter) {
187 return !IsAllowed(conversation_starter);
188 });
189
190 // When the server doesn't respond with any conversation starters that
191 // we can present, we'll fallback to the locally provided set.
192 if (conversation_starters.empty()) {
193 self->ProvideConversationStarters();
194 return;
195 }
196
197 // The number of conversation starters should not exceed our maximum.
198 while (conversation_starters.size() > kMaxNumOfConversationStarters)
199 conversation_starters.pop_back();
200
201 // We need to transform our conversation starters into the type that is
202 // understood by the suggestions model...
203 std::vector<AssistantSuggestion> suggestions;
204 std::transform(conversation_starters.begin(),
205 conversation_starters.end(),
206 std::back_inserter(suggestions), ToAssistantSuggestion);
207
208 // ...and we update our cache.
209 self->model_.SetConversationStarters(std::move(suggestions));
210 },
211 conversation_starters_weak_factory_.GetWeakPtr()));
212 }
213
ProvideConversationStarters()214 void AssistantSuggestionsControllerImpl::ProvideConversationStarters() {
215 std::vector<AssistantSuggestion> conversation_starters;
216
217 // Adds a conversation starter for the given |message_id| and |action_url|.
218 auto AddConversationStarter = [&conversation_starters](
219 int message_id, GURL action_url = GURL()) {
220 AssistantSuggestion starter;
221 starter.id = base::UnguessableToken::Create();
222 starter.type = AssistantSuggestionType::kConversationStarter;
223 starter.text = l10n_util::GetStringUTF8(message_id);
224 starter.action_url = action_url;
225 conversation_starters.push_back(std::move(starter));
226 };
227
228 // Always show the "What can you do?" conversation starter.
229 AddConversationStarter(IDS_ASH_ASSISTANT_CHIP_WHAT_CAN_YOU_DO);
230
231 // If enabled, always show the "What's on my screen?" conversation starter.
232 if (AssistantState::Get()->context_enabled().value_or(false)) {
233 AddConversationStarter(IDS_ASH_ASSISTANT_CHIP_WHATS_ON_MY_SCREEN,
234 assistant::util::CreateWhatsOnMyScreenDeepLink());
235 }
236
237 // The rest of the conversation starters will be shuffled...
238 std::vector<int> shuffled_message_ids;
239
240 shuffled_message_ids.push_back(IDS_ASH_ASSISTANT_CHIP_IM_BORED);
241 shuffled_message_ids.push_back(IDS_ASH_ASSISTANT_CHIP_OPEN_FILES);
242 shuffled_message_ids.push_back(IDS_ASH_ASSISTANT_CHIP_PLAY_MUSIC);
243 shuffled_message_ids.push_back(IDS_ASH_ASSISTANT_CHIP_SEND_AN_EMAIL);
244 shuffled_message_ids.push_back(IDS_ASH_ASSISTANT_CHIP_SET_A_REMINDER);
245 shuffled_message_ids.push_back(IDS_ASH_ASSISTANT_CHIP_WHATS_ON_MY_CALENDAR);
246 shuffled_message_ids.push_back(IDS_ASH_ASSISTANT_CHIP_WHATS_THE_WEATHER);
247
248 base::RandomShuffle(shuffled_message_ids.begin(), shuffled_message_ids.end());
249
250 // ...and added until we have no more than |kMaxNumOfConversationStarters|.
251 for (int i = 0;
252 conversation_starters.size() < kMaxNumOfConversationStarters &&
253 i < static_cast<int>(shuffled_message_ids.size());
254 ++i) {
255 AddConversationStarter(shuffled_message_ids[i]);
256 }
257
258 model_.SetConversationStarters(std::move(conversation_starters));
259 }
260
UpdateOnboardingSuggestions()261 void AssistantSuggestionsControllerImpl::UpdateOnboardingSuggestions() {
262 DCHECK(IsBetterOnboardingEnabled());
263
264 auto CreateIconResourceLink = [](int message_id) {
265 switch (message_id) {
266 case IDS_ASH_ASSISTANT_ONBOARDING_SUGGESTION_CONVERSION:
267 return assistant::util::CreateIconResourceLink(
268 assistant::util::IconName::kConversionPath);
269 case IDS_ASH_ASSISTANT_ONBOARDING_SUGGESTION_KNOWLEDGE:
270 return assistant::util::CreateIconResourceLink(
271 assistant::util::IconName::kPersonPinCircle);
272 case IDS_ASH_ASSISTANT_ONBOARDING_SUGGESTION_KNOWLEDGE_EDU:
273 return assistant::util::CreateIconResourceLink(
274 assistant::util::IconName::kStraighten);
275 case IDS_ASH_ASSISTANT_ONBOARDING_SUGGESTION_LANGUAGE:
276 return assistant::util::CreateIconResourceLink(
277 assistant::util::IconName::kTranslate);
278 case IDS_ASH_ASSISTANT_ONBOARDING_SUGGESTION_MATH:
279 return assistant::util::CreateIconResourceLink(
280 assistant::util::IconName::kCalculate);
281 case IDS_ASH_ASSISTANT_ONBOARDING_SUGGESTION_PERSONALITY:
282 return assistant::util::CreateIconResourceLink(
283 assistant::util::IconName::kSentimentVerySatisfied);
284 case IDS_ASH_ASSISTANT_ONBOARDING_SUGGESTION_PRODUCTIVITY:
285 return assistant::util::CreateIconResourceLink(
286 assistant::util::IconName::kTimer);
287 case IDS_ASH_ASSISTANT_ONBOARDING_SUGGESTION_TECHNICAL:
288 return assistant::util::CreateIconResourceLink(
289 assistant::util::IconName::kScreenshot);
290 default:
291 NOTREACHED();
292 return GURL();
293 }
294 };
295
296 std::vector<AssistantSuggestion> onboarding_suggestions;
297
298 using chromeos::assistant::AssistantBetterOnboardingType;
299 auto AddSuggestion = [&CreateIconResourceLink, &onboarding_suggestions](
300 int message_id, AssistantBetterOnboardingType type) {
301 onboarding_suggestions.emplace_back();
302 auto& suggestion = onboarding_suggestions.back();
303 suggestion.id = base::UnguessableToken::Create();
304 suggestion.type = AssistantSuggestionType::kBetterOnboarding;
305 suggestion.better_onboarding_type = type;
306 suggestion.text = l10n_util::GetStringUTF8(message_id);
307 suggestion.icon_url = CreateIconResourceLink(message_id);
308 suggestion.action_url = GURL();
309 };
310
311 switch (AssistantState::Get()->onboarding_mode().value_or(
312 AssistantOnboardingMode::kDefault)) {
313 case AssistantOnboardingMode::kEducation:
314 AddSuggestion(IDS_ASH_ASSISTANT_ONBOARDING_SUGGESTION_MATH,
315 AssistantBetterOnboardingType::kMath);
316 AddSuggestion(IDS_ASH_ASSISTANT_ONBOARDING_SUGGESTION_KNOWLEDGE_EDU,
317 AssistantBetterOnboardingType::kKnowledgeEdu);
318 break;
319 case AssistantOnboardingMode::kDefault:
320 AddSuggestion(IDS_ASH_ASSISTANT_ONBOARDING_SUGGESTION_CONVERSION,
321 AssistantBetterOnboardingType::kConversion);
322 AddSuggestion(IDS_ASH_ASSISTANT_ONBOARDING_SUGGESTION_KNOWLEDGE,
323 AssistantBetterOnboardingType::kKnowledge);
324 break;
325 }
326
327 AddSuggestion(IDS_ASH_ASSISTANT_ONBOARDING_SUGGESTION_PRODUCTIVITY,
328 AssistantBetterOnboardingType::kProductivity);
329 AddSuggestion(IDS_ASH_ASSISTANT_ONBOARDING_SUGGESTION_PERSONALITY,
330 AssistantBetterOnboardingType::kPersonality);
331 AddSuggestion(IDS_ASH_ASSISTANT_ONBOARDING_SUGGESTION_LANGUAGE,
332 AssistantBetterOnboardingType::kLanguage);
333 AddSuggestion(IDS_ASH_ASSISTANT_ONBOARDING_SUGGESTION_TECHNICAL,
334 AssistantBetterOnboardingType::kTechnical);
335
336 model_.SetOnboardingSuggestions(std::move(onboarding_suggestions));
337 }
338
339 } // namespace ash
340