blob: 72188109e0acc6e03a0d64c22d3e357b462b7371 [file] [log] [blame]
// Copyright 2014 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 "components/omnibox/browser/suggestion_answer.h"
#include <stddef.h>
#include <memory>
#include "base/feature_list.h"
#include "base/i18n/rtl.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/trace_event/memory_usage_estimator.h"
#include "base/values.h"
#include "net/base/escape.h"
#include "url/url_constants.h"
#ifdef OS_ANDROID
#include "base/android/jni_string.h"
#include "components/omnibox/browser/jni_headers/SuggestionAnswer_jni.h"
using base::android::ScopedJavaLocalRef;
#endif
namespace {
// All of these are defined here (even though most are only used once each) so
// the format details are easy to locate and update or compare to the spec doc.
static constexpr char kAnswerJsonLines[] = "l";
static constexpr char kAnswerJsonImageLine[] = "il";
static constexpr char kAnswerJsonText[] = "t";
static constexpr char kAnswerJsonAdditionalText[] = "at";
static constexpr char kAnswerJsonStatusText[] = "st";
static constexpr char kAnswerJsonTextType[] = "tt";
static constexpr char kAnswerJsonNumLines[] = "ln";
static constexpr char kAnswerJsonImage[] = "i";
static constexpr char kAnswerJsonImageData[] = "d";
void AppendWithSpace(const SuggestionAnswer::TextField* text,
std::u16string* output) {
if (!text)
return;
if (!output->empty() && !text->text().empty())
*output += ' ';
*output += text->text();
}
} // namespace
// SuggestionAnswer::TextField -------------------------------------------------
SuggestionAnswer::TextField::TextField() = default;
SuggestionAnswer::TextField::~TextField() = default;
SuggestionAnswer::TextField::TextField(const TextField&) = default;
SuggestionAnswer::TextField::TextField(TextField&&) noexcept = default;
SuggestionAnswer::TextField& SuggestionAnswer::TextField::operator=(
const TextField&) = default;
SuggestionAnswer::TextField& SuggestionAnswer::TextField::operator=(
TextField&&) noexcept = default;
// static
bool SuggestionAnswer::TextField::ParseTextField(const base::Value& field_json,
TextField* text_field) {
DCHECK(field_json.is_dict());
const std::string* text = field_json.FindStringKey(kAnswerJsonText);
absl::optional<int> type = field_json.FindIntKey(kAnswerJsonTextType);
const bool parsed = text && !text->empty() && type;
if (parsed) {
text_field->type_ = *type;
text_field->text_ = net::UnescapeForHTML(base::UTF8ToUTF16(*text));
absl::optional<int> num_lines = field_json.FindIntKey(kAnswerJsonNumLines);
text_field->has_num_lines_ = num_lines.has_value();
if (num_lines) {
text_field->has_num_lines_ = true;
text_field->num_lines_ = *num_lines;
} else
text_field->has_num_lines_ = false;
}
return parsed;
}
bool SuggestionAnswer::TextField::Equals(const TextField& field) const {
return type_ == field.type_ && text_ == field.text_ &&
has_num_lines_ == field.has_num_lines_ &&
(!has_num_lines_ || num_lines_ == field.num_lines_);
}
size_t SuggestionAnswer::TextField::EstimateMemoryUsage() const {
return base::trace_event::EstimateMemoryUsage(text_);
}
// SuggestionAnswer::ImageLine -------------------------------------------------
SuggestionAnswer::ImageLine::ImageLine()
: num_text_lines_(1) {}
SuggestionAnswer::ImageLine::ImageLine(const ImageLine& line) = default;
SuggestionAnswer::ImageLine::ImageLine(ImageLine&&) noexcept = default;
SuggestionAnswer::ImageLine& SuggestionAnswer::ImageLine::operator=(
const ImageLine& line) = default;
SuggestionAnswer::ImageLine& SuggestionAnswer::ImageLine::operator=(
ImageLine&&) noexcept = default;
SuggestionAnswer::ImageLine::~ImageLine() {}
// static
bool SuggestionAnswer::ImageLine::ParseImageLine(const base::Value& line_json,
ImageLine* image_line) {
DCHECK(line_json.is_dict());
const base::Value* inner_json = line_json.FindKeyOfType(
kAnswerJsonImageLine, base::Value::Type::DICTIONARY);
if (!inner_json)
return false;
const base::Value* fields_json =
inner_json->FindKeyOfType(kAnswerJsonText, base::Value::Type::LIST);
if (!fields_json || fields_json->GetList().empty())
return false;
bool found_num_lines = false;
for (const base::Value& field_json : fields_json->GetList()) {
TextField text_field;
if (!field_json.is_dict() ||
!TextField::ParseTextField(field_json, &text_field))
return false;
image_line->text_fields_.push_back(text_field);
if (!found_num_lines && text_field.has_num_lines()) {
found_num_lines = true;
image_line->num_text_lines_ = text_field.num_lines();
}
}
const base::Value* additional_text_json =
inner_json->FindKey(kAnswerJsonAdditionalText);
if (additional_text_json) {
image_line->additional_text_ = TextField();
if (!additional_text_json->is_dict() ||
!TextField::ParseTextField(*additional_text_json,
&image_line->additional_text_.value()))
return false;
}
const base::Value* status_text_json =
inner_json->FindKey(kAnswerJsonStatusText);
if (status_text_json) {
image_line->status_text_ = TextField();
if (!status_text_json->is_dict() ||
!TextField::ParseTextField(*status_text_json,
&image_line->status_text_.value()))
return false;
}
const base::Value* image_json = inner_json->FindKey(kAnswerJsonImage);
if (image_json) {
const std::string* url_string =
image_json->FindStringKey(kAnswerJsonImageData);
if (!url_string || url_string->empty())
return false;
// If necessary, concatenate scheme and host/path using only ':' as
// separator. This is due to the results delivering strings of the form
// "//host/path", which is web-speak for "use the enclosing page's scheme",
// but not a valid path of an URL. The GWS frontend commonly (always?)
// redirects to HTTPS so we just default to that here.
image_line->image_url_ =
GURL(base::StartsWith(*url_string, "//", base::CompareCase::SENSITIVE)
? (std::string(url::kHttpsScheme) + ":" + *url_string)
: *url_string);
if (!image_line->image_url_.is_valid())
return false;
}
return true;
}
bool SuggestionAnswer::ImageLine::Equals(const ImageLine& line) const {
if (text_fields_.size() != line.text_fields_.size())
return false;
for (size_t i = 0; i < text_fields_.size(); ++i) {
if (!text_fields_[i].Equals(line.text_fields_[i]))
return false;
}
if (num_text_lines_ != line.num_text_lines_)
return false;
if (additional_text_ || line.additional_text_) {
if (!additional_text_ || !line.additional_text_)
return false;
if (!additional_text_->Equals(*line.additional_text_))
return false;
}
if (status_text_ || line.status_text_) {
if (!status_text_ || !line.status_text_)
return false;
if (!status_text_->Equals(*line.status_text_))
return false;
}
return image_url_ == line.image_url_;
}
// TODO(jdonnelly): When updating the display of answers in RTL languages,
// modify this to be consistent.
std::u16string SuggestionAnswer::ImageLine::AccessibleText() const {
std::u16string result;
for (const TextField& text_field : text_fields_)
AppendWithSpace(&text_field, &result);
AppendWithSpace(additional_text(), &result);
AppendWithSpace(status_text(), &result);
return result;
}
size_t SuggestionAnswer::ImageLine::EstimateMemoryUsage() const {
size_t res = 0;
res += base::trace_event::EstimateMemoryUsage(text_fields_);
res += sizeof(int);
if (additional_text_)
res += base::trace_event::EstimateMemoryUsage(additional_text_.value());
else
res += sizeof(TextField);
res += sizeof(int);
if (status_text_)
res += base::trace_event::EstimateMemoryUsage(status_text_.value());
else
res += sizeof(TextField);
res += base::trace_event::EstimateMemoryUsage(image_url_);
return res;
}
void SuggestionAnswer::ImageLine::SetTextStyles(
int from_type,
SuggestionAnswer::TextStyle style) {
const auto replace = [=](auto* field) {
if (field->style() == TextStyle::NONE &&
(from_type == 0 || from_type == field->type())) {
field->set_style(style);
}
};
for (auto& field : text_fields_)
replace(&field);
if (additional_text_)
replace(&additional_text_.value());
if (status_text_)
replace(&status_text_.value());
}
// SuggestionAnswer ------------------------------------------------------------
SuggestionAnswer::SuggestionAnswer() = default;
SuggestionAnswer::SuggestionAnswer(const SuggestionAnswer& answer) = default;
SuggestionAnswer::SuggestionAnswer(SuggestionAnswer&&) noexcept = default;
SuggestionAnswer& SuggestionAnswer::operator=(const SuggestionAnswer& answer) =
default;
SuggestionAnswer& SuggestionAnswer::operator=(SuggestionAnswer&&) noexcept =
default;
SuggestionAnswer::~SuggestionAnswer() = default;
// static
bool SuggestionAnswer::ParseAnswer(const base::Value& answer_json,
const std::u16string& answer_type_str,
SuggestionAnswer* result) {
DCHECK(answer_json.is_dict());
int answer_type = 0;
if (!base::StringToInt(answer_type_str, &answer_type))
return false;
result->set_type(answer_type);
const base::Value* lines_json =
answer_json.FindKeyOfType(kAnswerJsonLines, base::Value::Type::LIST);
if (!lines_json || lines_json->GetList().size() != 2) {
return false;
}
const base::Value& first_line_json = lines_json->GetList()[0];
if (!first_line_json.is_dict() ||
!ImageLine::ParseImageLine(first_line_json, &result->first_line_)) {
return false;
}
const base::Value& second_line_json = lines_json->GetList()[1];
if (!second_line_json.is_dict() ||
!ImageLine::ParseImageLine(second_line_json, &result->second_line_)) {
return false;
}
const std::string* image_url;
const base::Value* optional_image =
answer_json.FindKeyOfType("i", base::Value::Type::DICTIONARY);
if (optional_image && (image_url = optional_image->FindStringKey("d"))) {
result->image_url_ = GURL(*image_url);
} else {
result->image_url_ = result->second_line_.image_url();
}
result->InterpretTextTypes();
return true;
}
bool SuggestionAnswer::Equals(const SuggestionAnswer& answer) const {
return type_ == answer.type_ && image_url_ == answer.image_url_ &&
first_line_.Equals(answer.first_line_) &&
second_line_.Equals(answer.second_line_);
}
void SuggestionAnswer::AddImageURLsTo(URLs* urls) const {
// Note: first_line_.image_url() is not used in practice (so it's ignored).
if (image_url_.is_valid())
urls->push_back(image_url_);
else if (second_line_.image_url().is_valid())
urls->push_back(second_line_.image_url());
}
size_t SuggestionAnswer::EstimateMemoryUsage() const {
size_t res = 0;
res += base::trace_event::EstimateMemoryUsage(image_url_);
res += base::trace_event::EstimateMemoryUsage(first_line_);
res += base::trace_event::EstimateMemoryUsage(second_line_);
return res;
}
void SuggestionAnswer::InterpretTextTypes() {
switch (type()) {
case SuggestionAnswer::ANSWER_TYPE_WEATHER: {
second_line_.SetTextStyles(SuggestionAnswer::TOP_ALIGNED,
TextStyle::SUPERIOR);
break;
}
case SuggestionAnswer::ANSWER_TYPE_FINANCE: {
first_line_.SetTextStyles(
SuggestionAnswer::SUGGESTION_SECONDARY_TEXT_SMALL,
TextStyle::SECONDARY);
second_line_.SetTextStyles(SuggestionAnswer::DESCRIPTION_POSITIVE,
TextStyle::POSITIVE);
second_line_.SetTextStyles(SuggestionAnswer::DESCRIPTION_NEGATIVE,
TextStyle::NEGATIVE);
break;
}
default:
break;
}
// Most answers uniformly apply different styling for each answer line.
// Any old styles not replaced above will get these by default.
if (IsExceptedFromLineReversal()) {
first_line_.SetTextStyles(0, TextStyle::NORMAL);
second_line_.SetTextStyles(0, TextStyle::NORMAL_DIM);
} else {
first_line_.SetTextStyles(0, TextStyle::NORMAL_DIM);
second_line_.SetTextStyles(0, TextStyle::NORMAL);
}
}
bool SuggestionAnswer::IsExceptedFromLineReversal() const {
return type() == SuggestionAnswer::ANSWER_TYPE_DICTIONARY;
}
// static
void SuggestionAnswer::LogAnswerUsed(
const absl::optional<SuggestionAnswer>& answer) {
auto answer_type = SuggestionAnswer::ANSWER_TYPE_INVALID;
if (answer) {
answer_type = static_cast<SuggestionAnswer::AnswerType>(answer->type());
}
DCHECK_NE(-1, answer_type); // just in case; |type_| is init'd to -1
UMA_HISTOGRAM_ENUMERATION(kAnswerUsedUmaHistogramName, answer_type,
SuggestionAnswer::ANSWER_TYPE_TOTAL_COUNT);
}
// static
const char SuggestionAnswer::kAnswerUsedUmaHistogramName[] =
"Omnibox.SuggestionUsed.AnswerInSuggest";
#ifdef OS_ANDROID
namespace {
ScopedJavaLocalRef<jobject> CreateJavaTextField(
JNIEnv* env,
const SuggestionAnswer::TextField& text_field) {
return Java_SuggestionAnswer_createTextField(
env, text_field.type(),
base::android::ConvertUTF16ToJavaString(env, text_field.text()),
static_cast<int>(text_field.style()), text_field.num_lines());
}
ScopedJavaLocalRef<jobject> CreateJavaImageLine(
JNIEnv* env,
const SuggestionAnswer::ImageLine* image_line) {
ScopedJavaLocalRef<jobject> jtext_fields =
Java_SuggestionAnswer_createTextFieldList(env);
for (const SuggestionAnswer::TextField& text_field :
image_line->text_fields()) {
Java_SuggestionAnswer_addTextFieldToList(
env, jtext_fields, CreateJavaTextField(env, text_field));
}
ScopedJavaLocalRef<jobject> jadditional_text;
if (image_line->additional_text())
jadditional_text = CreateJavaTextField(env, *image_line->additional_text());
ScopedJavaLocalRef<jobject> jstatus_text;
if (image_line->status_text())
jstatus_text = CreateJavaTextField(env, *image_line->status_text());
ScopedJavaLocalRef<jstring> jimage_url;
if (image_line->image_url().is_valid()) {
jimage_url = base::android::ConvertUTF8ToJavaString(
env, image_line->image_url().spec());
}
return Java_SuggestionAnswer_createImageLine(
env, jtext_fields, jadditional_text, jstatus_text, jimage_url);
}
} // namespace
ScopedJavaLocalRef<jobject> SuggestionAnswer::CreateJavaObject() const {
JNIEnv* env = base::android::AttachCurrentThread();
return Java_SuggestionAnswer_createSuggestionAnswer(
env, static_cast<int>(type_), CreateJavaImageLine(env, &first_line_),
CreateJavaImageLine(env, &second_line_));
}
#endif // OS_ANDROID