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