// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/assistant/model/assistant_response.h"
#include <utility>
#include "ash/assistant/model/assistant_response_observer.h"
#include "ash/assistant/model/ui/assistant_error_element.h"
#include "ash/assistant/model/ui/assistant_ui_element.h"
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/ranges/algorithm.h"
#include "base/unguessable_token.h"
#include "chromeos/ash/services/assistant/public/cpp/assistant_service.h"
#include "chromeos/ash/services/assistant/public/cpp/features.h"
namespace ash {
// AssistantResponse::PendingUiElement -----------------------------------------
struct AssistantResponse::PendingUiElement {
public:
PendingUiElement() = default;
~PendingUiElement() = default;
PendingUiElement(const PendingUiElement&) = delete;
PendingUiElement& operator=(const PendingUiElement&) = delete;
std::unique_ptr<AssistantUiElement> ui_element;
bool is_processing = false;
};
// AssistantResponse::Processor ------------------------------------------------
class AssistantResponse::Processor {
public:
Processor(AssistantResponse* response, ProcessingCallback callback)
: response_(response), callback_(std::move(callback)) {}
Processor(const Processor& copy) = delete;
Processor& operator=(const Processor& assign) = delete;
~Processor() {
if (callback_)
std::move(callback_).Run(/*is_completed=*/false);
}
void Process() {
// Responses should only be processed once.
DCHECK_EQ(ProcessingState::kUnprocessed, response_->processing_state());
response_->set_processing_state(ProcessingState::kProcessing);
// Completion of |response_| processing is indicated by |processing_count_|
// reaching zero. This value is decremented as each UI element is processed.
processing_count_ = response_->GetUiElements().size();
// Try finishing directly if there are no UI elements to be processed.
if (processing_count_ == 0) {
TryFinishing();
return;
}
for (const auto& ui_element : response_->GetUiElements()) {
// Start asynchronous processing of the UI element. Note that if the UI
// element does not require any pre-rendering processing the callback may
// be run synchronously. Also we must use WeakPtr here because |this| will
// destroy before |ui_element| by design.
ui_element->Process(
base::BindOnce(&AssistantResponse::Processor::OnFinishedProcessing,
weak_ptr_factory_.GetWeakPtr()));
}
}
private:
void OnFinishedProcessing() {
// We handle success/failure cases the same because failures will be skipped
// in view handling. We decrement our |processing_count_| and attempt to
// finish response processing. This will no-op if elements are still
// processing.
--processing_count_;
TryFinishing();
}
void TryFinishing() {
// No-op if we are already finished or if elements are still processing.
if (!callback_ || processing_count_ > 0)
return;
// Notify processing completion.
response_->set_processing_state(ProcessingState::kProcessed);
std::move(callback_).Run(/*is_completed=*/true);
}
// |response_| should outlive the Processor.
const raw_ptr<AssistantResponse> response_;
ProcessingCallback callback_;
int processing_count_ = 0;
base::WeakPtrFactory<AssistantResponse::Processor> weak_ptr_factory_{this};
};
// AssistantResponse -----------------------------------------------------------
AssistantResponse::AssistantResponse() = default;
AssistantResponse::~AssistantResponse() {
// Reset |processor_| explicitly in the destructor to guarantee the correct
// lifecycle where |this| should outlive the |processor_|. This can also force
// |processor_| to be destroyed before |ui_elements_| as we want regardless of
// the declaration order.
processor_.reset();
}
void AssistantResponse::AddObserver(AssistantResponseObserver* observer) const {
observers_.AddObserver(observer);
}
void AssistantResponse::RemoveObserver(
AssistantResponseObserver* observer) const {
observers_.RemoveObserver(observer);
}
void AssistantResponse::AddUiElement(
std::unique_ptr<AssistantUiElement> ui_element) {
// In processing v2, UI elements are first cached in a pending state...
auto pending_ui_element = std::make_unique<PendingUiElement>();
pending_ui_element->ui_element = std::move(ui_element);
pending_ui_element->is_processing = true;
pending_ui_elements_.push_back(std::move(pending_ui_element));
// ...while we perform any pre-processing necessary prior to rendering.
pending_ui_elements_.back()->ui_element->Process(base::BindOnce(
[](const base::WeakPtr<AssistantResponse>& self,
PendingUiElement* pending_ui_element) {
if (!self)
return;
// Indicate that |pending_ui_element| has finished processing.
pending_ui_element->is_processing = false;
// Add any UI elements that are ready for rendering to the response.
// Note that this may or may not include the |pending_ui_element| which
// just finished processing as we are required to add renderable UI
// elements to the response in the same order that they were initially
// pended to avoid inadvertently shuffling the response.
while (!self->pending_ui_elements_.empty() &&
!self->pending_ui_elements_.front()->is_processing) {
self->ui_elements_.push_back(
std::move(self->pending_ui_elements_.front()->ui_element));
self->pending_ui_elements_.pop_front();
self->NotifyUiElementAdded(self->ui_elements_.back().get());
}
},
weak_factory_.GetWeakPtr(),
base::Unretained(pending_ui_elements_.back().get())));
}
const std::vector<std::unique_ptr<AssistantUiElement>>&
AssistantResponse::GetUiElements() const {
return ui_elements_;
}
void AssistantResponse::AddSuggestions(
const std::vector<AssistantSuggestion>& suggestions) {
for (const auto& suggestion : suggestions)
suggestions_.push_back(suggestion);
NotifySuggestionsAdded(suggestions);
}
const assistant::AssistantSuggestion* AssistantResponse::GetSuggestionById(
const base::UnguessableToken& id) const {
for (auto& suggestion : suggestions_) {
if (suggestion.id == id)
return &suggestion;
}
return nullptr;
}
const std::vector<assistant::AssistantSuggestion>&
AssistantResponse::GetSuggestions() const {
return suggestions_;
}
void AssistantResponse::Process(ProcessingCallback callback) {
processor_ = std::make_unique<Processor>(this, std::move(callback));
processor_->Process();
}
void AssistantResponse::NotifyUiElementAdded(
const AssistantUiElement* ui_element) {
for (auto& observer : observers_)
observer.OnUiElementAdded(ui_element);
}
void AssistantResponse::NotifySuggestionsAdded(
const std::vector<AssistantSuggestion>& suggestions) {
for (auto& observer : observers_)
observer.OnSuggestionsAdded(suggestions);
}
bool AssistantResponse::ContainsUiElement(
const AssistantUiElement* element) const {
DCHECK(element);
bool contains_element = base::Contains(
ui_elements_, *element, &std::unique_ptr<AssistantUiElement>::operator*);
return contains_element || ContainsPendingUiElement(element);
}
bool AssistantResponse::ContainsPendingUiElement(
const AssistantUiElement* element) const {
DCHECK(element);
return base::ranges::any_of(
pending_ui_elements_,
[element](const std::unique_ptr<PendingUiElement>& other) {
return *other->ui_element == *element;
});
}
} // namespace ash