| // 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/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 "components/omnibox/browser/omnibox_field_trial.h" |
| #include "net/base/escape.h" |
| #include "url/url_constants.h" |
| |
| #ifdef OS_ANDROID |
| #include "base/android/jni_string.h" |
| #include "jni/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[] = "i.d"; |
| |
| void AppendWithSpace(const SuggestionAnswer::TextField* text, |
| base::string16* 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; |
| |
| // static |
| bool SuggestionAnswer::TextField::ParseTextField( |
| const base::DictionaryValue* field_json, TextField* text_field) { |
| bool parsed = field_json->GetString(kAnswerJsonText, &text_field->text_) && |
| !text_field->text_.empty() && |
| field_json->GetInteger(kAnswerJsonTextType, &text_field->type_); |
| if (parsed) { |
| text_field->text_ = net::UnescapeForHTML(text_field->text_); |
| text_field->has_num_lines_ = |
| field_json->GetInteger(kAnswerJsonNumLines, &text_field->num_lines_); |
| } |
| 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& SuggestionAnswer::ImageLine::operator=( |
| const ImageLine& line) = default; |
| |
| SuggestionAnswer::ImageLine::~ImageLine() {} |
| |
| // static |
| bool SuggestionAnswer::ImageLine::ParseImageLine( |
| const base::DictionaryValue* line_json, ImageLine* image_line) { |
| const base::DictionaryValue* inner_json; |
| if (!line_json->GetDictionary(kAnswerJsonImageLine, &inner_json)) |
| return false; |
| |
| const base::ListValue* fields_json; |
| if (!inner_json->GetList(kAnswerJsonText, &fields_json) || |
| fields_json->GetSize() == 0) |
| return false; |
| |
| bool found_num_lines = false; |
| for (size_t i = 0; i < fields_json->GetSize(); ++i) { |
| const base::DictionaryValue* field_json; |
| TextField text_field; |
| if (!fields_json->GetDictionary(i, &field_json) || |
| !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(); |
| } |
| } |
| |
| if (inner_json->HasKey(kAnswerJsonAdditionalText)) { |
| image_line->additional_text_ = TextField(); |
| const base::DictionaryValue* field_json; |
| if (!inner_json->GetDictionary(kAnswerJsonAdditionalText, &field_json) || |
| !TextField::ParseTextField(field_json, |
| &image_line->additional_text_.value())) |
| return false; |
| } |
| |
| if (inner_json->HasKey(kAnswerJsonStatusText)) { |
| image_line->status_text_ = TextField(); |
| const base::DictionaryValue* field_json; |
| if (!inner_json->GetDictionary(kAnswerJsonStatusText, &field_json) || |
| !TextField::ParseTextField(field_json, |
| &image_line->status_text_.value())) |
| return false; |
| } |
| |
| if (inner_json->HasKey(kAnswerJsonImage)) { |
| base::string16 url_string; |
| if (!inner_json->GetString(kAnswerJsonImageData, &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::ASCIIToUTF16("//"), |
| base::CompareCase::SENSITIVE) |
| ? (base::ASCIIToUTF16(url::kHttpsScheme) + |
| base::ASCIIToUTF16(":") + 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. |
| base::string16 SuggestionAnswer::ImageLine::AccessibleText() const { |
| base::string16 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::operator=(const SuggestionAnswer& answer) = |
| default; |
| |
| SuggestionAnswer::~SuggestionAnswer() = default; |
| |
| // static |
| bool SuggestionAnswer::ParseAnswer(const base::DictionaryValue* answer_json, |
| const base::string16& answer_type_str, |
| SuggestionAnswer* result) { |
| int answer_type = 0; |
| if (!base::StringToInt(answer_type_str, &answer_type)) |
| return false; |
| |
| result->set_type(answer_type); |
| |
| const base::ListValue* lines_json; |
| if (!answer_json->GetList(kAnswerJsonLines, &lines_json) || |
| lines_json->GetSize() != 2) { |
| return false; |
| } |
| |
| const base::DictionaryValue* first_line_json; |
| if (!lines_json->GetDictionary(0, &first_line_json) || |
| !ImageLine::ParseImageLine(first_line_json, &result->first_line_)) { |
| return false; |
| } |
| |
| const base::DictionaryValue* second_line_json; |
| if (!lines_json->GetDictionary(1, &second_line_json) || |
| !ImageLine::ParseImageLine(second_line_json, &result->second_line_)) { |
| return false; |
| } |
| |
| std::string image_url; |
| const base::DictionaryValue* optional_image; |
| if (OmniboxFieldTrial::IsNewAnswerLayoutEnabled() && |
| answer_json->GetDictionary("i", &optional_image) && |
| optional_image->GetString("d", &image_url)) { |
| 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() { |
| if (!OmniboxFieldTrial::IsNewAnswerLayoutEnabled()) |
| return; |
| |
| 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); |
| second_line_.SetTextStyles(SuggestionAnswer::ANSWER_TEXT_LARGE, |
| TextStyle::BOLD); |
| break; |
| } |
| case SuggestionAnswer::ANSWER_TYPE_DICTIONARY: { |
| // Because dictionary answers are excepted from line reversal, they |
| // get the expected normal first line and dim second line. |
| first_line_.SetTextStyles(0, TextStyle::NORMAL); |
| second_line_.SetTextStyles(0, TextStyle::NORMAL_DIM); |
| break; |
| } |
| default: |
| break; |
| } |
| |
| // Most answers uniformly apply different styling for each answer line. |
| // Any old styles not replaced above will get these by default. |
| first_line_.SetTextStyles(0, TextStyle::NORMAL_DIM); |
| second_line_.SetTextStyles(0, TextStyle::NORMAL); |
| } |
| |
| #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 |