| OLD | NEW |
| (Empty) | |
| 1 // Copyright 2016 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 "components/ntp_snippets/remote/ntp_snippets_json_request.h" |
| 6 |
| 7 #include <algorithm> |
| 8 #include <utility> |
| 9 #include <vector> |
| 10 |
| 11 #include "base/command_line.h" |
| 12 #include "base/json/json_writer.h" |
| 13 #include "base/memory/ptr_util.h" |
| 14 #include "base/metrics/histogram_macros.h" |
| 15 #include "base/metrics/sparse_histogram.h" |
| 16 #include "base/strings/stringprintf.h" |
| 17 #include "base/time/tick_clock.h" |
| 18 #include "base/time/time.h" |
| 19 #include "base/values.h" |
| 20 #include "components/data_use_measurement/core/data_use_user_data.h" |
| 21 #include "components/ntp_snippets/category_info.h" |
| 22 #include "components/ntp_snippets/features.h" |
| 23 #include "components/ntp_snippets/remote/ntp_snippets_request_params.h" |
| 24 #include "components/ntp_snippets/user_classifier.h" |
| 25 #include "components/signin/core/browser/profile_oauth2_token_service.h" |
| 26 #include "components/signin/core/browser/signin_manager.h" |
| 27 #include "components/signin/core/browser/signin_manager_base.h" |
| 28 #include "components/variations/net/variations_http_headers.h" |
| 29 #include "components/variations/variations_associated_data.h" |
| 30 #include "grit/components_strings.h" |
| 31 #include "net/base/load_flags.h" |
| 32 #include "net/http/http_response_headers.h" |
| 33 #include "net/http/http_status_code.h" |
| 34 #include "net/url_request/url_fetcher.h" |
| 35 #include "net/url_request/url_request_context_getter.h" |
| 36 #include "third_party/icu/source/common/unicode/uloc.h" |
| 37 #include "third_party/icu/source/common/unicode/utypes.h" |
| 38 #include "ui/base/l10n/l10n_util.h" |
| 39 |
| 40 using net::URLFetcher; |
| 41 using net::URLRequestContextGetter; |
| 42 using net::HttpRequestHeaders; |
| 43 using net::URLRequestStatus; |
| 44 using translate::LanguageModel; |
| 45 |
| 46 namespace ntp_snippets { |
| 47 |
| 48 namespace internal { |
| 49 |
| 50 namespace { |
| 51 |
| 52 // Variation parameter for disabling the retry. |
| 53 const char kBackground5xxRetriesName[] = "background_5xx_retries_count"; |
| 54 |
| 55 const int kMaxExcludedIds = 100; |
| 56 |
| 57 // Variation parameter for sending LanguageModel info to the server. |
| 58 const char kSendTopLanguagesName[] = "send_top_languages"; |
| 59 |
| 60 // Variation parameter for sending UserClassifier info to the server. |
| 61 const char kSendUserClassName[] = "send_user_class"; |
| 62 |
| 63 int Get5xxRetryCount(bool interactive_request) { |
| 64 if (interactive_request) { |
| 65 return 2; |
| 66 } |
| 67 return std::max(0, variations::GetVariationParamByFeatureAsInt( |
| 68 ntp_snippets::kArticleSuggestionsFeature, |
| 69 kBackground5xxRetriesName, 0)); |
| 70 } |
| 71 |
| 72 bool IsSendingTopLanguagesEnabled() { |
| 73 return variations::GetVariationParamByFeatureAsBool( |
| 74 ntp_snippets::kArticleSuggestionsFeature, kSendTopLanguagesName, |
| 75 /*default_value=*/false); |
| 76 } |
| 77 |
| 78 bool IsSendingUserClassEnabled() { |
| 79 return variations::GetVariationParamByFeatureAsBool( |
| 80 ntp_snippets::kArticleSuggestionsFeature, kSendUserClassName, |
| 81 /*default_value=*/false); |
| 82 } |
| 83 |
| 84 // Translate the BCP 47 |language_code| into a posix locale string. |
| 85 std::string PosixLocaleFromBCP47Language(const std::string& language_code) { |
| 86 char locale[ULOC_FULLNAME_CAPACITY]; |
| 87 UErrorCode error = U_ZERO_ERROR; |
| 88 // Translate the input to a posix locale. |
| 89 uloc_forLanguageTag(language_code.c_str(), locale, ULOC_FULLNAME_CAPACITY, |
| 90 nullptr, &error); |
| 91 if (error != U_ZERO_ERROR) { |
| 92 DLOG(WARNING) << "Error in translating language code to a locale string: " |
| 93 << error; |
| 94 return std::string(); |
| 95 } |
| 96 return locale; |
| 97 } |
| 98 |
| 99 std::string ISO639FromPosixLocale(const std::string& locale) { |
| 100 char language[ULOC_LANG_CAPACITY]; |
| 101 UErrorCode error = U_ZERO_ERROR; |
| 102 uloc_getLanguage(locale.c_str(), language, ULOC_LANG_CAPACITY, &error); |
| 103 if (error != U_ZERO_ERROR) { |
| 104 DLOG(WARNING) |
| 105 << "Error in translating locale string to a ISO639 language code: " |
| 106 << error; |
| 107 return std::string(); |
| 108 } |
| 109 return language; |
| 110 } |
| 111 |
| 112 void AppendLanguageInfoToList(base::ListValue* list, |
| 113 const LanguageModel::LanguageInfo& info) { |
| 114 auto lang = base::MakeUnique<base::DictionaryValue>(); |
| 115 lang->SetString("language", info.language_code); |
| 116 lang->SetDouble("frequency", info.frequency); |
| 117 list->Append(std::move(lang)); |
| 118 } |
| 119 |
| 120 std::string GetUserClassString(UserClassifier::UserClass user_class) { |
| 121 switch (user_class) { |
| 122 case UserClassifier::UserClass::RARE_NTP_USER: |
| 123 return "RARE_NTP_USER"; |
| 124 case UserClassifier::UserClass::ACTIVE_NTP_USER: |
| 125 return "ACTIVE_NTP_USER"; |
| 126 case UserClassifier::UserClass::ACTIVE_SUGGESTIONS_CONSUMER: |
| 127 return "ACTIVE_SUGGESTIONS_CONSUMER"; |
| 128 } |
| 129 NOTREACHED(); |
| 130 return std::string(); |
| 131 } |
| 132 |
| 133 } // namespace |
| 134 |
| 135 CategoryInfo BuildArticleCategoryInfo( |
| 136 const base::Optional<base::string16>& title) { |
| 137 return CategoryInfo( |
| 138 title.has_value() ? title.value() |
| 139 : l10n_util::GetStringUTF16( |
| 140 IDS_NTP_ARTICLE_SUGGESTIONS_SECTION_HEADER), |
| 141 ContentSuggestionsCardLayout::FULL_CARD, |
| 142 // TODO(dgn): merge has_more_action and has_reload_action when we remove |
| 143 // the kFetchMoreFeature flag. See https://crbug.com/667752 |
| 144 /*has_more_action=*/base::FeatureList::IsEnabled(kFetchMoreFeature), |
| 145 /*has_reload_action=*/true, |
| 146 /*has_view_all_action=*/false, |
| 147 /*show_if_empty=*/true, |
| 148 l10n_util::GetStringUTF16(IDS_NTP_ARTICLE_SUGGESTIONS_SECTION_EMPTY)); |
| 149 } |
| 150 |
| 151 CategoryInfo BuildRemoteCategoryInfo(const base::string16& title, |
| 152 bool allow_fetching_more_results) { |
| 153 return CategoryInfo( |
| 154 title, ContentSuggestionsCardLayout::FULL_CARD, |
| 155 // TODO(dgn): merge has_more_action and has_reload_action when we remove |
| 156 // the kFetchMoreFeature flag. See https://crbug.com/667752 |
| 157 /*has_more_action=*/allow_fetching_more_results && |
| 158 base::FeatureList::IsEnabled(kFetchMoreFeature), |
| 159 /*has_reload_action=*/allow_fetching_more_results, |
| 160 /*has_view_all_action=*/false, |
| 161 /*show_if_empty=*/false, |
| 162 // TODO(tschumann): The message for no-articles is likely wrong |
| 163 // and needs to be added to the stubby protocol if we want to |
| 164 // support it. |
| 165 l10n_util::GetStringUTF16(IDS_NTP_ARTICLE_SUGGESTIONS_SECTION_EMPTY)); |
| 166 } |
| 167 |
| 168 NTPSnippetsJsonRequest::NTPSnippetsJsonRequest( |
| 169 base::Optional<Category> exclusive_category, |
| 170 base::TickClock* tick_clock, // Needed until destruction of the request. |
| 171 const ParseJSONCallback& callback) |
| 172 : exclusive_category_(exclusive_category), |
| 173 tick_clock_(tick_clock), |
| 174 parse_json_callback_(callback), |
| 175 weak_ptr_factory_(this) { |
| 176 creation_time_ = tick_clock_->NowTicks(); |
| 177 } |
| 178 |
| 179 NTPSnippetsJsonRequest::~NTPSnippetsJsonRequest() { |
| 180 LOG_IF(DFATAL, !request_completed_callback_.is_null()) |
| 181 << "The CompletionCallback was never called!"; |
| 182 } |
| 183 |
| 184 void NTPSnippetsJsonRequest::Start(CompletedCallback callback) { |
| 185 request_completed_callback_ = std::move(callback); |
| 186 url_fetcher_->Start(); |
| 187 } |
| 188 |
| 189 base::TimeDelta NTPSnippetsJsonRequest::GetFetchDuration() const { |
| 190 return tick_clock_->NowTicks() - creation_time_; |
| 191 } |
| 192 |
| 193 std::string NTPSnippetsJsonRequest::GetResponseString() const { |
| 194 std::string response; |
| 195 url_fetcher_->GetResponseAsString(&response); |
| 196 return response; |
| 197 } |
| 198 |
| 199 //////////////////////////////////////////////////////////////////////////////// |
| 200 // URLFetcherDelegate overrides |
| 201 void NTPSnippetsJsonRequest::OnURLFetchComplete(const net::URLFetcher* source) { |
| 202 DCHECK_EQ(url_fetcher_.get(), source); |
| 203 const URLRequestStatus& status = url_fetcher_->GetStatus(); |
| 204 int response = url_fetcher_->GetResponseCode(); |
| 205 UMA_HISTOGRAM_SPARSE_SLOWLY( |
| 206 "NewTabPage.Snippets.FetchHttpResponseOrErrorCode", |
| 207 status.is_success() ? response : status.error()); |
| 208 |
| 209 if (!status.is_success()) { |
| 210 std::move(request_completed_callback_) |
| 211 .Run(/*result=*/nullptr, FetchResult::URL_REQUEST_STATUS_ERROR, |
| 212 /*error_details=*/base::StringPrintf(" %d", status.error())); |
| 213 } else if (response != net::HTTP_OK) { |
| 214 // TODO(jkrcal): https://crbug.com/609084 |
| 215 // We need to deal with the edge case again where the auth |
| 216 // token expires just before we send the request (in which case we need to |
| 217 // fetch a new auth token). We should extract that into a common class |
| 218 // instead of adding it to every single class that uses auth tokens. |
| 219 std::move(request_completed_callback_) |
| 220 .Run(/*result=*/nullptr, FetchResult::HTTP_ERROR, |
| 221 /*error_details=*/base::StringPrintf(" %d", response)); |
| 222 } else { |
| 223 ParseJsonResponse(); |
| 224 } |
| 225 } |
| 226 |
| 227 void NTPSnippetsJsonRequest::ParseJsonResponse() { |
| 228 std::string json_string; |
| 229 bool stores_result_to_string = |
| 230 url_fetcher_->GetResponseAsString(&json_string); |
| 231 DCHECK(stores_result_to_string); |
| 232 |
| 233 parse_json_callback_.Run(json_string, |
| 234 base::Bind(&NTPSnippetsJsonRequest::OnJsonParsed, |
| 235 weak_ptr_factory_.GetWeakPtr()), |
| 236 base::Bind(&NTPSnippetsJsonRequest::OnJsonError, |
| 237 weak_ptr_factory_.GetWeakPtr())); |
| 238 } |
| 239 |
| 240 void NTPSnippetsJsonRequest::OnJsonParsed(std::unique_ptr<base::Value> result) { |
| 241 std::move(request_completed_callback_) |
| 242 .Run(std::move(result), FetchResult::SUCCESS, |
| 243 /*error_details=*/std::string()); |
| 244 } |
| 245 |
| 246 void NTPSnippetsJsonRequest::OnJsonError(const std::string& error) { |
| 247 std::string json_string; |
| 248 url_fetcher_->GetResponseAsString(&json_string); |
| 249 LOG(WARNING) << "Received invalid JSON (" << error << "): " << json_string; |
| 250 std::move(request_completed_callback_) |
| 251 .Run(/*result=*/nullptr, FetchResult::JSON_PARSE_ERROR, |
| 252 /*error_details=*/base::StringPrintf(" (error %s)", error.c_str())); |
| 253 } |
| 254 |
| 255 NTPSnippetsJsonRequest::Builder::Builder() |
| 256 : fetch_api_(CHROME_READER_API), |
| 257 personalization_(Personalization::kBoth), |
| 258 language_model_(nullptr) {} |
| 259 NTPSnippetsJsonRequest::Builder::Builder(NTPSnippetsJsonRequest::Builder&&) = |
| 260 default; |
| 261 NTPSnippetsJsonRequest::Builder::~Builder() = default; |
| 262 |
| 263 std::unique_ptr<NTPSnippetsJsonRequest> NTPSnippetsJsonRequest::Builder::Build() |
| 264 const { |
| 265 DCHECK(!url_.is_empty()); |
| 266 DCHECK(url_request_context_getter_); |
| 267 DCHECK(tick_clock_); |
| 268 auto request = base::MakeUnique<NTPSnippetsJsonRequest>( |
| 269 params_.exclusive_category, tick_clock_, parse_json_callback_); |
| 270 std::string body = BuildBody(); |
| 271 std::string headers = BuildHeaders(); |
| 272 request->url_fetcher_ = BuildURLFetcher(request.get(), headers, body); |
| 273 |
| 274 // Log the request for debugging network issues. |
| 275 VLOG(1) << "Sending a NTP snippets request to " << url_ << ":\n" |
| 276 << headers << "\n" |
| 277 << body; |
| 278 |
| 279 return request; |
| 280 } |
| 281 |
| 282 NTPSnippetsJsonRequest::Builder& |
| 283 NTPSnippetsJsonRequest::Builder::SetAuthentication( |
| 284 const std::string& account_id, |
| 285 const std::string& auth_header) { |
| 286 obfuscated_gaia_id_ = account_id; |
| 287 auth_header_ = auth_header; |
| 288 return *this; |
| 289 } |
| 290 |
| 291 NTPSnippetsJsonRequest::Builder& NTPSnippetsJsonRequest::Builder::SetFetchAPI( |
| 292 FetchAPI fetch_api) { |
| 293 fetch_api_ = fetch_api; |
| 294 return *this; |
| 295 } |
| 296 |
| 297 NTPSnippetsJsonRequest::Builder& |
| 298 NTPSnippetsJsonRequest::Builder::SetLanguageModel( |
| 299 const translate::LanguageModel* language_model) { |
| 300 language_model_ = language_model; |
| 301 return *this; |
| 302 } |
| 303 |
| 304 NTPSnippetsJsonRequest::Builder& NTPSnippetsJsonRequest::Builder::SetParams( |
| 305 const NTPSnippetsRequestParams& params) { |
| 306 params_ = params; |
| 307 return *this; |
| 308 } |
| 309 |
| 310 NTPSnippetsJsonRequest::Builder& |
| 311 NTPSnippetsJsonRequest::Builder::SetParseJsonCallback( |
| 312 ParseJSONCallback callback) { |
| 313 parse_json_callback_ = callback; |
| 314 return *this; |
| 315 } |
| 316 |
| 317 NTPSnippetsJsonRequest::Builder& |
| 318 NTPSnippetsJsonRequest::Builder::SetPersonalization( |
| 319 Personalization personalization) { |
| 320 personalization_ = personalization; |
| 321 return *this; |
| 322 } |
| 323 |
| 324 NTPSnippetsJsonRequest::Builder& NTPSnippetsJsonRequest::Builder::SetTickClock( |
| 325 base::TickClock* tick_clock) { |
| 326 tick_clock_ = tick_clock; |
| 327 return *this; |
| 328 } |
| 329 |
| 330 NTPSnippetsJsonRequest::Builder& NTPSnippetsJsonRequest::Builder::SetUrl( |
| 331 const GURL& url) { |
| 332 url_ = url; |
| 333 return *this; |
| 334 } |
| 335 |
| 336 NTPSnippetsJsonRequest::Builder& |
| 337 NTPSnippetsJsonRequest::Builder::SetUrlRequestContextGetter( |
| 338 const scoped_refptr<net::URLRequestContextGetter>& context_getter) { |
| 339 url_request_context_getter_ = context_getter; |
| 340 return *this; |
| 341 } |
| 342 |
| 343 NTPSnippetsJsonRequest::Builder& |
| 344 NTPSnippetsJsonRequest::Builder::SetUserClassifier( |
| 345 const UserClassifier& user_classifier) { |
| 346 if (IsSendingUserClassEnabled()) { |
| 347 user_class_ = GetUserClassString(user_classifier.GetUserClass()); |
| 348 } |
| 349 return *this; |
| 350 } |
| 351 |
| 352 std::string NTPSnippetsJsonRequest::Builder::BuildHeaders() const { |
| 353 net::HttpRequestHeaders headers; |
| 354 headers.SetHeader("Content-Type", "application/json; charset=UTF-8"); |
| 355 if (!auth_header_.empty()) { |
| 356 headers.SetHeader("Authorization", auth_header_); |
| 357 } |
| 358 // Add X-Client-Data header with experiment IDs from field trials. |
| 359 // Note: It's fine to pass in |is_signed_in| false, which does not affect |
| 360 // transmission of experiment ids coming from the variations server. |
| 361 bool is_signed_in = false; |
| 362 variations::AppendVariationHeaders(url_, |
| 363 false, // incognito |
| 364 false, // uma_enabled |
| 365 is_signed_in, &headers); |
| 366 return headers.ToString(); |
| 367 } |
| 368 |
| 369 std::string NTPSnippetsJsonRequest::Builder::BuildBody() const { |
| 370 auto request = base::MakeUnique<base::DictionaryValue>(); |
| 371 std::string user_locale = PosixLocaleFromBCP47Language(params_.language_code); |
| 372 switch (fetch_api_) { |
| 373 case CHROME_READER_API: { |
| 374 auto content_params = base::MakeUnique<base::DictionaryValue>(); |
| 375 content_params->SetBoolean("only_return_personalized_results", |
| 376 ReturnOnlyPersonalizedResults()); |
| 377 |
| 378 auto content_restricts = base::MakeUnique<base::ListValue>(); |
| 379 for (const auto* metadata : {"TITLE", "SNIPPET", "THUMBNAIL"}) { |
| 380 auto entry = base::MakeUnique<base::DictionaryValue>(); |
| 381 entry->SetString("type", "METADATA"); |
| 382 entry->SetString("value", metadata); |
| 383 content_restricts->Append(std::move(entry)); |
| 384 } |
| 385 |
| 386 auto local_scoring_params = base::MakeUnique<base::DictionaryValue>(); |
| 387 local_scoring_params->Set("content_params", std::move(content_params)); |
| 388 local_scoring_params->Set("content_restricts", |
| 389 std::move(content_restricts)); |
| 390 |
| 391 auto global_scoring_params = base::MakeUnique<base::DictionaryValue>(); |
| 392 global_scoring_params->SetInteger("num_to_return", |
| 393 params_.count_to_fetch); |
| 394 global_scoring_params->SetInteger("sort_type", 1); |
| 395 |
| 396 auto advanced = base::MakeUnique<base::DictionaryValue>(); |
| 397 advanced->Set("local_scoring_params", std::move(local_scoring_params)); |
| 398 advanced->Set("global_scoring_params", std::move(global_scoring_params)); |
| 399 |
| 400 request->SetString("response_detail_level", "STANDARD"); |
| 401 request->Set("advanced_options", std::move(advanced)); |
| 402 if (!obfuscated_gaia_id_.empty()) { |
| 403 request->SetString("obfuscated_gaia_id", obfuscated_gaia_id_); |
| 404 } |
| 405 if (!user_locale.empty()) { |
| 406 request->SetString("user_locale", user_locale); |
| 407 } |
| 408 break; |
| 409 } |
| 410 |
| 411 case CHROME_CONTENT_SUGGESTIONS_API: { |
| 412 if (!user_locale.empty()) { |
| 413 request->SetString("uiLanguage", user_locale); |
| 414 } |
| 415 |
| 416 request->SetString("priority", params_.interactive_request |
| 417 ? "USER_ACTION" |
| 418 : "BACKGROUND_PREFETCH"); |
| 419 |
| 420 auto excluded = base::MakeUnique<base::ListValue>(); |
| 421 for (const auto& id : params_.excluded_ids) { |
| 422 excluded->AppendString(id); |
| 423 if (excluded->GetSize() >= kMaxExcludedIds) { |
| 424 break; |
| 425 } |
| 426 } |
| 427 request->Set("excludedSuggestionIds", std::move(excluded)); |
| 428 |
| 429 if (!user_class_.empty()) { |
| 430 request->SetString("userActivenessClass", user_class_); |
| 431 } |
| 432 |
| 433 translate::LanguageModel::LanguageInfo ui_language; |
| 434 translate::LanguageModel::LanguageInfo other_top_language; |
| 435 PrepareLanguages(&ui_language, &other_top_language); |
| 436 |
| 437 if (ui_language.frequency == 0 && other_top_language.frequency == 0) { |
| 438 break; |
| 439 } |
| 440 |
| 441 auto language_list = base::MakeUnique<base::ListValue>(); |
| 442 if (ui_language.frequency > 0) { |
| 443 AppendLanguageInfoToList(language_list.get(), ui_language); |
| 444 } |
| 445 if (other_top_language.frequency > 0) { |
| 446 AppendLanguageInfoToList(language_list.get(), other_top_language); |
| 447 } |
| 448 request->Set("topLanguages", std::move(language_list)); |
| 449 |
| 450 // TODO(sfiera): Support only_return_personalized_results. |
| 451 // TODO(sfiera): Support count_to_fetch. |
| 452 break; |
| 453 } |
| 454 } |
| 455 |
| 456 std::string request_json; |
| 457 bool success = base::JSONWriter::WriteWithOptions( |
| 458 *request, base::JSONWriter::OPTIONS_PRETTY_PRINT, &request_json); |
| 459 DCHECK(success); |
| 460 return request_json; |
| 461 } |
| 462 |
| 463 std::unique_ptr<net::URLFetcher> |
| 464 NTPSnippetsJsonRequest::Builder::BuildURLFetcher( |
| 465 net::URLFetcherDelegate* delegate, |
| 466 const std::string& headers, |
| 467 const std::string& body) const { |
| 468 std::unique_ptr<net::URLFetcher> url_fetcher = |
| 469 net::URLFetcher::Create(url_, net::URLFetcher::POST, delegate); |
| 470 url_fetcher->SetRequestContext(url_request_context_getter_.get()); |
| 471 url_fetcher->SetLoadFlags(net::LOAD_DO_NOT_SEND_COOKIES | |
| 472 net::LOAD_DO_NOT_SAVE_COOKIES); |
| 473 data_use_measurement::DataUseUserData::AttachToFetcher( |
| 474 url_fetcher.get(), data_use_measurement::DataUseUserData::NTP_SNIPPETS); |
| 475 |
| 476 url_fetcher->SetExtraRequestHeaders(headers); |
| 477 url_fetcher->SetUploadData("application/json", body); |
| 478 |
| 479 // Fetchers are sometimes cancelled because a network change was detected. |
| 480 url_fetcher->SetAutomaticallyRetryOnNetworkChanges(3); |
| 481 url_fetcher->SetMaxRetriesOn5xx( |
| 482 Get5xxRetryCount(params_.interactive_request)); |
| 483 return url_fetcher; |
| 484 } |
| 485 |
| 486 void NTPSnippetsJsonRequest::Builder::PrepareLanguages( |
| 487 translate::LanguageModel::LanguageInfo* ui_language, |
| 488 translate::LanguageModel::LanguageInfo* other_top_language) const { |
| 489 // TODO(jkrcal): Add language model factory for iOS and add fakes to tests so |
| 490 // that |language_model| is never nullptr. Remove this check and add a DCHECK |
| 491 // into the constructor. |
| 492 if (!language_model_ || !IsSendingTopLanguagesEnabled()) { |
| 493 return; |
| 494 } |
| 495 |
| 496 // TODO(jkrcal): Is this back-and-forth converting necessary? |
| 497 ui_language->language_code = ISO639FromPosixLocale( |
| 498 PosixLocaleFromBCP47Language(params_.language_code)); |
| 499 ui_language->frequency = |
| 500 language_model_->GetLanguageFrequency(ui_language->language_code); |
| 501 |
| 502 std::vector<LanguageModel::LanguageInfo> top_languages = |
| 503 language_model_->GetTopLanguages(); |
| 504 for (const LanguageModel::LanguageInfo& info : top_languages) { |
| 505 if (info.language_code != ui_language->language_code) { |
| 506 *other_top_language = info; |
| 507 |
| 508 // Report to UMA how important the UI language is. |
| 509 DCHECK_GT(other_top_language->frequency, 0) |
| 510 << "GetTopLanguages() should not return languages with 0 frequency"; |
| 511 float ratio_ui_in_both_languages = |
| 512 ui_language->frequency / |
| 513 (ui_language->frequency + other_top_language->frequency); |
| 514 UMA_HISTOGRAM_PERCENTAGE( |
| 515 "NewTabPage.Languages.UILanguageRatioInTwoTopLanguages", |
| 516 ratio_ui_in_both_languages * 100); |
| 517 break; |
| 518 } |
| 519 } |
| 520 } |
| 521 |
| 522 } // namespace internal |
| 523 |
| 524 } // namespace ntp_snippets |
| OLD | NEW |