Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(523)

Side by Side Diff: chrome/browser/autocomplete/history_url_provider.cc

Issue 336173005: Don't call AutocompleteInput::Parse() on a background thread, part 1. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Created 6 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "chrome/browser/autocomplete/history_url_provider.h" 5 #include "chrome/browser/autocomplete/history_url_provider.h"
6 6
7 #include <algorithm> 7 #include <algorithm>
8 8
9 #include "base/basictypes.h" 9 #include "base/basictypes.h"
10 #include "base/bind.h" 10 #include "base/bind.h"
(...skipping 18 matching lines...) Expand all
29 #include "chrome/browser/profiles/profile.h" 29 #include "chrome/browser/profiles/profile.h"
30 #include "chrome/browser/search_engines/template_url_service.h" 30 #include "chrome/browser/search_engines/template_url_service.h"
31 #include "chrome/browser/search_engines/template_url_service_factory.h" 31 #include "chrome/browser/search_engines/template_url_service_factory.h"
32 #include "chrome/browser/search_engines/ui_thread_search_terms_data.h" 32 #include "chrome/browser/search_engines/ui_thread_search_terms_data.h"
33 #include "chrome/common/chrome_switches.h" 33 #include "chrome/common/chrome_switches.h"
34 #include "chrome/common/pref_names.h" 34 #include "chrome/common/pref_names.h"
35 #include "chrome/common/url_constants.h" 35 #include "chrome/common/url_constants.h"
36 #include "components/bookmarks/browser/bookmark_utils.h" 36 #include "components/bookmarks/browser/bookmark_utils.h"
37 #include "components/metrics/proto/omnibox_input_type.pb.h" 37 #include "components/metrics/proto/omnibox_input_type.pb.h"
38 #include "components/url_fixer/url_fixer.h" 38 #include "components/url_fixer/url_fixer.h"
39 #include "content/public/browser/browser_thread.h"
39 #include "net/base/net_util.h" 40 #include "net/base/net_util.h"
40 #include "net/base/registry_controlled_domains/registry_controlled_domain.h" 41 #include "net/base/registry_controlled_domains/registry_controlled_domain.h"
41 #include "url/gurl.h" 42 #include "url/gurl.h"
42 #include "url/url_parse.h" 43 #include "url/url_parse.h"
43 #include "url/url_util.h" 44 #include "url/url_util.h"
44 45
45 namespace { 46 namespace {
46 47
47 // Acts like the > operator for URLInfo classes. 48 // Acts like the > operator for URLInfo classes.
48 bool CompareHistoryMatch(const history::HistoryMatch& a, 49 bool CompareHistoryMatch(const history::HistoryMatch& a,
(...skipping 172 matching lines...) Expand 10 before | Expand all | Expand 10 after
221 // No entry, so create one. 222 // No entry, so create one.
222 history::HistoryMatch match(info, input_location, match_in_scheme, true); 223 history::HistoryMatch match(info, input_location, match_in_scheme, true);
223 if (promote) 224 if (promote)
224 matches->push_front(match); 225 matches->push_front(match);
225 else 226 else
226 matches->push_back(match); 227 matches->push_back(match);
227 228
228 return true; 229 return true;
229 } 230 }
230 231
232 // Returns whether |match| is suitable for inline autocompletion.
233 bool CanPromoteMatchForInlineAutocomplete(const history::HistoryMatch& match) {
234 // We can promote this match if it's been marked for promotion or typed at
235 // least n times, where n == 1 for "simple" (host-only) URLs and n == 2 for
236 // others. We set a higher bar for these long URLs because it's less likely
237 // that users will want to visit them again. Even though we don't increment
238 // the typed_count for pasted-in URLs, if the user manually edits the URL or
239 // types some long thing in by hand, we wouldn't want to immediately start
240 // autocompleting it.
241 return match.promoted ||
242 (match.url_info.typed_count() &&
243 ((match.url_info.typed_count() > 1) || match.IsHostOnly()));
244 }
245
231 // Given the user's |input| and a |match| created from it, reduce the match's 246 // Given the user's |input| and a |match| created from it, reduce the match's
232 // URL to just a host. If this host still matches the user input, return it. 247 // URL to just a host. If this host still matches the user input, return it.
233 // Returns the empty string on failure. 248 // Returns the empty string on failure.
234 GURL ConvertToHostOnly(const history::HistoryMatch& match, 249 GURL ConvertToHostOnly(const history::HistoryMatch& match,
235 const base::string16& input) { 250 const base::string16& input) {
236 // See if we should try to do host-only suggestions for this URL. Nonstandard 251 // See if we should try to do host-only suggestions for this URL. Nonstandard
237 // schemes means there's no authority section, so suggesting the host name 252 // schemes means there's no authority section, so suggesting the host name
238 // is useless. File URLs are standard, but host suggestion is not useful for 253 // is useless. File URLs are standard, but host suggestion is not useful for
239 // them either. 254 // them either.
240 const GURL& url = match.url_info.url(); 255 const GURL& url = match.url_info.url();
(...skipping 143 matching lines...) Expand 10 before | Expand all | Expand 10 after
384 // The user typed an intranet hostname that they've visited (albeit with a 399 // The user typed an intranet hostname that they've visited (albeit with a
385 // different port and/or path) before. 400 // different port and/or path) before.
386 url_row_ = history::URLRow(url); 401 url_row_ = history::URLRow(url);
387 type_ = UNVISITED_INTRANET; 402 type_ = UNVISITED_INTRANET;
388 } 403 }
389 } 404 }
390 405
391 HistoryURLProviderParams::HistoryURLProviderParams( 406 HistoryURLProviderParams::HistoryURLProviderParams(
392 const AutocompleteInput& input, 407 const AutocompleteInput& input,
393 bool trim_http, 408 bool trim_http,
409 const AutocompleteMatch& what_you_typed_match,
394 const std::string& languages, 410 const std::string& languages,
395 TemplateURL* default_search_provider, 411 TemplateURL* default_search_provider,
396 const SearchTermsData& search_terms_data) 412 const SearchTermsData& search_terms_data)
397 : message_loop(base::MessageLoop::current()), 413 : message_loop(base::MessageLoop::current()),
398 input(input), 414 input(input),
399 prevent_inline_autocomplete(input.prevent_inline_autocomplete()), 415 prevent_inline_autocomplete(input.prevent_inline_autocomplete()),
400 trim_http(trim_http), 416 trim_http(trim_http),
417 what_you_typed_match(what_you_typed_match),
401 failed(false), 418 failed(false),
402 languages(languages), 419 languages(languages),
403 dont_suggest_exact_input(false), 420 dont_suggest_exact_input(false),
404 default_search_provider(default_search_provider ? 421 default_search_provider(default_search_provider ?
405 new TemplateURL(default_search_provider->profile(), 422 new TemplateURL(default_search_provider->profile(),
406 default_search_provider->data()) : NULL), 423 default_search_provider->data()) : NULL),
407 search_terms_data(new SearchTermsDataSnapshot(search_terms_data)) { 424 search_terms_data(new SearchTermsDataSnapshot(search_terms_data)) {
408 } 425 }
409 426
410 HistoryURLProviderParams::~HistoryURLProviderParams() { 427 HistoryURLProviderParams::~HistoryURLProviderParams() {
411 } 428 }
412 429
413 HistoryURLProvider::HistoryURLProvider(AutocompleteProviderListener* listener, 430 HistoryURLProvider::HistoryURLProvider(AutocompleteProviderListener* listener,
414 Profile* profile) 431 Profile* profile)
415 : HistoryProvider(listener, profile, 432 : HistoryProvider(listener, profile,
416 AutocompleteProvider::TYPE_HISTORY_URL), 433 AutocompleteProvider::TYPE_HISTORY_URL),
417 params_(NULL), 434 params_(NULL),
418 cull_redirects_( 435 cull_redirects_(
419 !OmniboxFieldTrial::InHUPCullRedirectsFieldTrial() || 436 !OmniboxFieldTrial::InHUPCullRedirectsFieldTrial() ||
420 !OmniboxFieldTrial::InHUPCullRedirectsFieldTrialExperimentGroup()), 437 !OmniboxFieldTrial::InHUPCullRedirectsFieldTrialExperimentGroup()),
421 create_shorter_match_( 438 create_shorter_match_(
422 !OmniboxFieldTrial::InHUPCreateShorterMatchFieldTrial() || 439 !OmniboxFieldTrial::InHUPCreateShorterMatchFieldTrial() ||
423 !OmniboxFieldTrial:: 440 !OmniboxFieldTrial::
424 InHUPCreateShorterMatchFieldTrialExperimentGroup()) { 441 InHUPCreateShorterMatchFieldTrialExperimentGroup()) {
425 // Initialize HUP scoring params based on the current experiment. 442 // Initialize HUP scoring params based on the current experiment.
426 OmniboxFieldTrial::GetExperimentalHUPScoringParams(&scoring_params_); 443 OmniboxFieldTrial::GetExperimentalHUPScoringParams(&scoring_params_);
(...skipping 12 matching lines...) Expand all
439 456
440 // Cancel any in-progress query. 457 // Cancel any in-progress query.
441 Stop(false); 458 Stop(false);
442 459
443 matches_.clear(); 460 matches_.clear();
444 461
445 if ((input.type() == metrics::OmniboxInputType::INVALID) || 462 if ((input.type() == metrics::OmniboxInputType::INVALID) ||
446 (input.type() == metrics::OmniboxInputType::FORCED_QUERY)) 463 (input.type() == metrics::OmniboxInputType::FORCED_QUERY))
447 return; 464 return;
448 465
449 // Create a match for exactly what the user typed. This will only be used as 466 // Do some fixup on the user input before matching against it, so we provide
450 // a fallback in case we can't get the history service or URL DB; otherwise, 467 // good results for local file paths, input with spaces, etc.
451 // we'll run this again in DoAutocomplete() and use that result instead. 468 const FixupReturn fixup_return(FixupUserInput(input));
469 if (!fixup_return.first)
470 return;
Mark P 2014/06/17 17:43:25 This makes me nervous. Previously we'd create a a
Peter Kasting 2014/06/17 20:39:47 It wasn't conscious. The two pieces were written
Mark P 2014/06/17 21:35:35 Good point.
471 url::Parsed parts;
472 url_fixer::SegmentURL(fixup_return.second, &parts);
473 AutocompleteInput fixed_up_input(input);
474 fixed_up_input.UpdateText(fixup_return.second, base::string16::npos, parts);
475
476 // Create a match for what the user typed.
452 const bool trim_http = !AutocompleteInput::HasHTTPScheme(input.text()); 477 const bool trim_http = !AutocompleteInput::HasHTTPScheme(input.text());
453 // Don't do this for queries -- while we can sometimes mark up a match for 478 AutocompleteMatch what_you_typed_match(SuggestExactInput(
454 // this, it's not what the user wants, and just adds noise. 479 fixed_up_input.text(), fixed_up_input.canonicalized_url(), trim_http));
455 if (input.type() != metrics::OmniboxInputType::QUERY) { 480 what_you_typed_match.relevance = CalculateRelevance(WHAT_YOU_TYPED, 0);
456 AutocompleteMatch what_you_typed(SuggestExactInput( 481
457 input.text(), input.canonicalized_url(), trim_http)); 482 // Add the WYT match as a fallback in case we can't get the history service or
458 what_you_typed.relevance = CalculateRelevance(WHAT_YOU_TYPED, 0); 483 // URL DB; otherwise, we'll replace this match lower down. Don't do this for
459 matches_.push_back(what_you_typed); 484 // queries, though -- while we can sometimes mark up a match for them, it's
460 } 485 // not what the user wants, and just adds noise.
486 if (fixed_up_input.type() != metrics::OmniboxInputType::QUERY)
487 matches_.push_back(what_you_typed_match);
461 488
462 // We'll need the history service to run both passes, so try to obtain it. 489 // We'll need the history service to run both passes, so try to obtain it.
463 if (!profile_) 490 if (!profile_)
464 return; 491 return;
465 HistoryService* const history_service = 492 HistoryService* const history_service =
466 HistoryServiceFactory::GetForProfile(profile_, Profile::EXPLICIT_ACCESS); 493 HistoryServiceFactory::GetForProfile(profile_, Profile::EXPLICIT_ACCESS);
467 if (!history_service) 494 if (!history_service)
468 return; 495 return;
469 496
470 // Get the default search provider and search terms data now since we have to 497 // Get the default search provider and search terms data now since we have to
471 // retrieve these on the UI thread, and the second pass runs on the history 498 // retrieve these on the UI thread, and the second pass runs on the history
472 // thread. |template_url_service| can be NULL when testing. 499 // thread. |template_url_service| can be NULL when testing.
473 TemplateURLService* template_url_service = 500 TemplateURLService* template_url_service =
474 TemplateURLServiceFactory::GetForProfile(profile_); 501 TemplateURLServiceFactory::GetForProfile(profile_);
475 TemplateURL* default_search_provider = template_url_service ? 502 TemplateURL* default_search_provider = template_url_service ?
476 template_url_service->GetDefaultSearchProvider() : NULL; 503 template_url_service->GetDefaultSearchProvider() : NULL;
477 UIThreadSearchTermsData data(profile_); 504 UIThreadSearchTermsData data(profile_);
478 505
479 // Do some fixup on the user input before matching against it, so we provide
480 // good results for local file paths, input with spaces, etc.
481 const FixupReturn fixup_return(FixupUserInput(input));
482 if (!fixup_return.first)
483 return;
484 url::Parsed parts;
485 url_fixer::SegmentURL(fixup_return.second, &parts);
486 AutocompleteInput fixed_up_input(input);
487 fixed_up_input.UpdateText(fixup_return.second, base::string16::npos, parts);
488
489 // Create the data structure for the autocomplete passes. We'll save this off 506 // Create the data structure for the autocomplete passes. We'll save this off
490 // onto the |params_| member for later deletion below if we need to run pass 507 // onto the |params_| member for later deletion below if we need to run pass
491 // 2. 508 // 2.
492 scoped_ptr<HistoryURLProviderParams> params( 509 scoped_ptr<HistoryURLProviderParams> params(
493 new HistoryURLProviderParams( 510 new HistoryURLProviderParams(
494 fixed_up_input, trim_http, 511 fixed_up_input, trim_http, what_you_typed_match,
495 profile_->GetPrefs()->GetString(prefs::kAcceptLanguages), 512 profile_->GetPrefs()->GetString(prefs::kAcceptLanguages),
496 default_search_provider, data)); 513 default_search_provider, data));
497 // Note that we use the non-fixed-up input here, since fixup may strip 514 // Note that we use the non-fixed-up input here, since fixup may strip
498 // trailing whitespace. 515 // trailing whitespace.
499 params->prevent_inline_autocomplete = PreventInlineAutocomplete(input); 516 params->prevent_inline_autocomplete = PreventInlineAutocomplete(input);
500 517
501 // Pass 1: Get the in-memory URL database, and use it to find and promote 518 // Pass 1: Get the in-memory URL database, and use it to find and promote
502 // the inline autocomplete match, if any. 519 // the inline autocomplete match, if any.
503 history::URLDatabase* url_db = history_service->InMemoryDatabase(); 520 history::URLDatabase* url_db = history_service->InMemoryDatabase();
504 // url_db can be NULL if it hasn't finished initializing (or failed to 521 // url_db can be NULL if it hasn't finished initializing (or failed to
505 // initialize). In this case all we can do is fall back on the second 522 // initialize). In this case all we can do is fall back on the second
506 // pass. 523 // pass.
507 // 524 //
508 // TODO(pkasting): We should just block here until this loads. Any time 525 // TODO(pkasting): We should just block here until this loads. Any time
509 // someone unloads the history backend, we'll get inconsistent inline 526 // someone unloads the history backend, we'll get inconsistent inline
510 // autocomplete behavior here. 527 // autocomplete behavior here.
511 if (url_db) { 528 if (url_db) {
512 DoAutocomplete(NULL, url_db, params.get()); 529 DoAutocomplete(NULL, url_db, params.get());
513 // params->matches now has the matches we should expose to the provider. 530 // params->matches now has the matches we should expose to the provider.
514 // Pass 2 expects a "clean slate" set of matches. 531 // Pass 2 expects a "clean slate" set of matches.
515 matches_.clear(); 532 matches_.clear();
516 matches_.swap(params->matches); 533 matches_.swap(params->matches);
517 UpdateStarredStateOfMatches(); 534 UpdateStarredStateOfMatches();
535 params->what_you_typed_match = what_you_typed_match;
Mark P 2014/06/17 17:43:25 I don't understand why this is necessary. Didn't
Peter Kasting 2014/06/17 20:39:48 Running an autocomplete pass changes the What You
Mark P 2014/06/17 21:35:35 This is not an obvious side effect of DoAutocomple
518 } 536 }
519 537
520 // Pass 2: Ask the history service to call us back on the history thread, 538 // Pass 2: Ask the history service to call us back on the history thread,
521 // where we can read the full on-disk DB. 539 // where we can read the full on-disk DB.
522 if (input.want_asynchronous_matches()) { 540 if (input.want_asynchronous_matches()) {
523 done_ = false; 541 done_ = false;
524 params_ = params.release(); // This object will be destroyed in 542 params_ = params.release(); // This object will be destroyed in
525 // QueryComplete() once we're done with it. 543 // QueryComplete() once we're done with it.
526 history_service->ScheduleAutocomplete(this, params_); 544 history_service->ScheduleAutocomplete(this, params_);
527 } 545 }
528 } 546 }
529 547
530 void HistoryURLProvider::Stop(bool clear_cached_results) { 548 void HistoryURLProvider::Stop(bool clear_cached_results) {
531 done_ = true; 549 done_ = true;
532 550
533 if (params_) 551 if (params_)
534 params_->cancel_flag.Set(); 552 params_->cancel_flag.Set();
535 } 553 }
536 554
537 AutocompleteMatch HistoryURLProvider::SuggestExactInput( 555 AutocompleteMatch HistoryURLProvider::SuggestExactInput(
538 const base::string16& text, 556 const base::string16& text,
539 const GURL& destination_url, 557 const GURL& destination_url,
540 bool trim_http) { 558 bool trim_http) {
559 // The FormattedStringWithEquivalentMeaning() call below requires callers to
560 // be on the UI thread.
561 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI) ||
562 !content::BrowserThread::IsMessageLoopValid(content::BrowserThread::UI));
Mark P 2014/06/17 17:43:25 elsewhere we usually use IsThreadInitialized rathe
Peter Kasting 2014/06/17 20:39:47 Done.
563
541 AutocompleteMatch match(this, 0, false, 564 AutocompleteMatch match(this, 0, false,
542 AutocompleteMatchType::URL_WHAT_YOU_TYPED); 565 AutocompleteMatchType::URL_WHAT_YOU_TYPED);
543 566
544 if (destination_url.is_valid()) { 567 if (destination_url.is_valid()) {
545 match.destination_url = destination_url; 568 match.destination_url = destination_url;
546 569
547 // Trim off "http://" if the user didn't type it. 570 // Trim off "http://" if the user didn't type it.
548 // NOTE: We use TrimHttpPrefix() here rather than StringForURLDisplay() to 571 // NOTE: We use TrimHttpPrefix() here rather than StringForURLDisplay() to
549 // strip the scheme as we need to know the offset so we can adjust the 572 // strip the scheme as we need to know the offset so we can adjust the
550 // |match_location| below. StringForURLDisplay() and TrimHttpPrefix() have 573 // |match_location| below. StringForURLDisplay() and TrimHttpPrefix() have
(...skipping 105 matching lines...) Expand 10 before | Expand all | Expand 10 after
656 history::ScoredHistoryMatch::FilterTermMatchesByWordStarts( 679 history::ScoredHistoryMatch::FilterTermMatchesByWordStarts(
657 description_matches, offsets, description_word_starts, 0, 680 description_matches, offsets, description_word_starts, 0,
658 std::string::npos); 681 std::string::npos);
659 return SpansFromTermMatch( 682 return SpansFromTermMatch(
660 description_matches, clean_description.length(), false); 683 description_matches, clean_description.length(), false);
661 } 684 }
662 685
663 void HistoryURLProvider::DoAutocomplete(history::HistoryBackend* backend, 686 void HistoryURLProvider::DoAutocomplete(history::HistoryBackend* backend,
664 history::URLDatabase* db, 687 history::URLDatabase* db,
665 HistoryURLProviderParams* params) { 688 HistoryURLProviderParams* params) {
666 VisitClassifier classifier(this, params->input, db); 689 // Get the matching URLs from the DB.
667 // Create a What You Typed match, which we'll need below.
668 //
669 // We display this to the user when there's a reasonable chance they actually
670 // care:
671 // * Their input can be opened as a URL, and
672 // * We parsed the input as a URL, or it starts with an explicit "http:" or
673 // "https:".
674 // that is when their input can be opened as a URL.
675 // Otherwise, this is just low-quality noise. In the cases where we've parsed
676 // as UNKNOWN, we'll still show an accidental search infobar if need be.
677 bool have_what_you_typed_match =
678 (params->input.type() != metrics::OmniboxInputType::QUERY) &&
679 ((params->input.type() != metrics::OmniboxInputType::UNKNOWN) ||
680 (classifier.type() == VisitClassifier::UNVISITED_INTRANET) ||
681 !params->trim_http ||
682 (AutocompleteInput::NumNonHostComponents(params->input.parts()) > 0));
683 AutocompleteMatch what_you_typed_match(SuggestExactInput(
684 params->input.text(), params->input.canonicalized_url(),
685 params->trim_http));
686 what_you_typed_match.relevance = CalculateRelevance(WHAT_YOU_TYPED, 0);
687
688 // Get the matching URLs from the DB
689 history::URLRows url_matches; 690 history::URLRows url_matches;
690 history::HistoryMatches history_matches; 691 history::HistoryMatches history_matches;
691 692
692 const URLPrefixes& prefixes = URLPrefix::GetURLPrefixes(); 693 const URLPrefixes& prefixes = URLPrefix::GetURLPrefixes();
693 for (URLPrefixes::const_iterator i(prefixes.begin()); i != prefixes.end(); 694 for (URLPrefixes::const_iterator i(prefixes.begin()); i != prefixes.end();
694 ++i) { 695 ++i) {
695 if (params->cancel_flag.IsSet()) 696 if (params->cancel_flag.IsSet())
696 return; // Canceled in the middle of a query, give up. 697 return; // Canceled in the middle of a query, give up.
698
697 // We only need kMaxMatches results in the end, but before we get there we 699 // We only need kMaxMatches results in the end, but before we get there we
698 // need to promote lower-quality matches that are prefixes of higher-quality 700 // need to promote lower-quality matches that are prefixes of higher-quality
699 // matches, and remove lower-quality redirects. So we ask for more results 701 // matches, and remove lower-quality redirects. So we ask for more results
700 // than we need, of every prefix type, in hopes this will give us far more 702 // than we need, of every prefix type, in hopes this will give us far more
701 // than enough to work with. CullRedirects() will then reduce the list to 703 // than enough to work with. CullRedirects() will then reduce the list to
702 // the best kMaxMatches results. 704 // the best kMaxMatches results.
703 db->AutocompleteForPrefix( 705 db->AutocompleteForPrefix(
704 base::UTF16ToUTF8(i->prefix + params->input.text()), 706 base::UTF16ToUTF8(i->prefix + params->input.text()), kMaxMatches * 2,
705 kMaxMatches * 2, 707 !backend, &url_matches);
706 (backend == NULL),
707 &url_matches);
708 for (history::URLRows::const_iterator j(url_matches.begin()); 708 for (history::URLRows::const_iterator j(url_matches.begin());
709 j != url_matches.end(); ++j) { 709 j != url_matches.end(); ++j) {
710 const URLPrefix* best_prefix = 710 const URLPrefix* best_prefix = URLPrefix::BestURLPrefix(
711 URLPrefix::BestURLPrefix(base::UTF8ToUTF16(j->url().spec()), 711 base::UTF8ToUTF16(j->url().spec()), base::string16());
712 base::string16()); 712 DCHECK(best_prefix);
713 DCHECK(best_prefix != NULL); 713 history_matches.push_back(history::HistoryMatch(
714 history_matches.push_back(history::HistoryMatch(*j, i->prefix.length(), 714 *j, i->prefix.length(), !i->num_components,
715 i->num_components == 0,
716 i->num_components >= best_prefix->num_components)); 715 i->num_components >= best_prefix->num_components));
717 } 716 }
718 } 717 }
719 718
720 // Create sorted list of suggestions. 719 // Create sorted list of suggestions.
721 CullPoorMatches(*params, &history_matches); 720 CullPoorMatches(*params, &history_matches);
722 SortAndDedupMatches(&history_matches); 721 SortAndDedupMatches(&history_matches);
722
723 // Try to create a shorter suggestion from the best match.
724 // We allow the what you typed match to be displayed when there's a reasonable
725 // chance the user actually cares:
726 // * Their input can be opened as a URL, and
727 // * We parsed the input as a URL, or it starts with an explicit "http:" or
728 // "https:".
729 // Otherwise, this is just low-quality noise. In the cases where we've parsed
730 // as UNKNOWN, we'll still show an accidental search infobar if need be.
731 VisitClassifier classifier(this, params->input, db);
732 bool have_what_you_typed_match =
733 (params->input.type() != metrics::OmniboxInputType::QUERY) &&
734 ((params->input.type() != metrics::OmniboxInputType::UNKNOWN) ||
735 (classifier.type() == VisitClassifier::UNVISITED_INTRANET) ||
736 !params->trim_http ||
737 (AutocompleteInput::NumNonHostComponents(params->input.parts()) > 0));
723 PromoteOrCreateShorterSuggestion(db, *params, have_what_you_typed_match, 738 PromoteOrCreateShorterSuggestion(db, *params, have_what_you_typed_match,
724 what_you_typed_match, &history_matches); 739 &history_matches);
725 740
726 // Try to promote a match as an exact/inline autocomplete match. This also 741 // Try to promote a match as an exact/inline autocomplete match. This also
727 // moves it to the front of |history_matches|, so skip over it when 742 // moves it to the front of |history_matches|, so skip over it when
728 // converting the rest of the matches. 743 // converting the rest of the matches.
729 size_t first_match = 1; 744 size_t first_match = 1;
730 size_t exact_suggestion = 0; 745 size_t exact_suggestion = 0;
731 // Checking |is_history_what_you_typed_match| tells us whether 746 // Checking params->what_you_typed_match.is_history_what_you_typed_match tells
732 // SuggestExactInput() succeeded in constructing a valid match. 747 // us whether SuggestExactInput() succeeded in constructing a valid match.
733 if (what_you_typed_match.is_history_what_you_typed_match && 748 if (params->what_you_typed_match.is_history_what_you_typed_match &&
734 (!backend || !params->dont_suggest_exact_input) && 749 (!backend || !params->dont_suggest_exact_input) &&
735 FixupExactSuggestion(db, params->input, classifier, &what_you_typed_match, 750 FixupExactSuggestion(db, classifier, params, &history_matches)) {
736 &history_matches)) {
737 // Got an exact match for the user's input. Treat it as the best match 751 // Got an exact match for the user's input. Treat it as the best match
738 // regardless of the input type. 752 // regardless of the input type.
739 exact_suggestion = 1; 753 exact_suggestion = 1;
740 params->matches.push_back(what_you_typed_match); 754 params->matches.push_back(params->what_you_typed_match);
741 } else if (params->prevent_inline_autocomplete || 755 } else if (params->prevent_inline_autocomplete ||
742 history_matches.empty() || 756 history_matches.empty() ||
743 !PromoteMatchForInlineAutocomplete(history_matches.front(), params)) { 757 !PromoteMatchForInlineAutocomplete(history_matches.front(), params)) {
744 // Failed to promote any URLs for inline autocompletion. Use the What You 758 // Failed to promote any URLs for inline autocompletion. Use the What You
745 // Typed match, if we have it. 759 // Typed match, if we have it.
746 first_match = 0; 760 first_match = 0;
747 if (have_what_you_typed_match) 761 if (have_what_you_typed_match)
748 params->matches.push_back(what_you_typed_match); 762 params->matches.push_back(params->what_you_typed_match);
749 } 763 }
750 764
751 // This is the end of the synchronous pass. 765 // This is the end of the synchronous pass.
752 if (!backend) 766 if (!backend)
753 return; 767 return;
754 768
755 // Determine relevancy of highest scoring match, if any. 769 // Determine relevancy of highest scoring match, if any.
756 int relevance = -1; 770 int relevance = -1;
757 for (ACMatches::const_iterator it = params->matches.begin(); 771 for (ACMatches::const_iterator it = params->matches.begin();
758 it != params->matches.end(); ++it) { 772 it != params->matches.end(); ++it) {
(...skipping 28 matching lines...) Expand all
787 // The experimental scoring must not change the top result's score. 801 // The experimental scoring must not change the top result's score.
788 if (!params->matches.empty()) { 802 if (!params->matches.empty()) {
789 relevance = CalculateRelevanceScoreUsingScoringParams(match, relevance, 803 relevance = CalculateRelevanceScoreUsingScoringParams(match, relevance,
790 scoring_params_); 804 scoring_params_);
791 ac_match.relevance = relevance; 805 ac_match.relevance = relevance;
792 } 806 }
793 params->matches.push_back(ac_match); 807 params->matches.push_back(ac_match);
794 } 808 }
795 } 809 }
796 810
797 // Called on the main thread when the query is complete.
Mark P 2014/06/17 17:43:25 Is this incorrect? Why did you remove it?
Peter Kasting 2014/06/17 20:39:47 This comment is already present in the header.
798 void HistoryURLProvider::QueryComplete( 811 void HistoryURLProvider::QueryComplete(
799 HistoryURLProviderParams* params_gets_deleted) { 812 HistoryURLProviderParams* params_gets_deleted) {
800 // Ensure |params_gets_deleted| gets deleted on exit. 813 // Ensure |params_gets_deleted| gets deleted on exit.
801 scoped_ptr<HistoryURLProviderParams> params(params_gets_deleted); 814 scoped_ptr<HistoryURLProviderParams> params(params_gets_deleted);
802 815
803 // If the user hasn't already started another query, clear our member pointer 816 // If the user hasn't already started another query, clear our member pointer
804 // so we can't write into deleted memory. 817 // so we can't write into deleted memory.
805 if (params_ == params_gets_deleted) 818 if (params_ == params_gets_deleted)
806 params_ = NULL; 819 params_ = NULL;
807 820
808 // Don't send responses for queries that have been canceled. 821 // Don't send responses for queries that have been canceled.
809 if (params->cancel_flag.IsSet()) 822 if (params->cancel_flag.IsSet())
810 return; // Already set done_ when we canceled, no need to set it again. 823 return; // Already set done_ when we canceled, no need to set it again.
811 824
812 // Don't modify |matches_| if the query failed, since it might have a default 825 // Don't modify |matches_| if the query failed, since it might have a default
813 // match in it, whereas |params->matches| will be empty. 826 // match in it, whereas |params->matches| will be empty.
814 if (!params->failed) { 827 if (!params->failed) {
815 matches_.swap(params->matches); 828 matches_.swap(params->matches);
816 UpdateStarredStateOfMatches(); 829 UpdateStarredStateOfMatches();
817 } 830 }
818 831
819 done_ = true; 832 done_ = true;
820 listener_->OnProviderUpdate(true); 833 listener_->OnProviderUpdate(true);
821 } 834 }
822 835
823 bool HistoryURLProvider::FixupExactSuggestion( 836 bool HistoryURLProvider::FixupExactSuggestion(
824 history::URLDatabase* db, 837 history::URLDatabase* db,
825 const AutocompleteInput& input,
826 const VisitClassifier& classifier, 838 const VisitClassifier& classifier,
827 AutocompleteMatch* match, 839 HistoryURLProviderParams* params,
828 history::HistoryMatches* matches) const { 840 history::HistoryMatches* matches) const {
829 DCHECK(match != NULL);
830 DCHECK(matches != NULL); 841 DCHECK(matches != NULL);
Mark P 2014/06/17 17:43:25 Perhaps DCHECK(params!=NULL)
Peter Kasting 2014/06/17 20:39:48 I could, but I don't think it adds much. I intend
831 842
832 MatchType type = INLINE_AUTOCOMPLETE; 843 MatchType type = INLINE_AUTOCOMPLETE;
833 switch (classifier.type()) { 844 switch (classifier.type()) {
834 case VisitClassifier::INVALID: 845 case VisitClassifier::INVALID:
835 return false; 846 return false;
836 case VisitClassifier::UNVISITED_INTRANET: 847 case VisitClassifier::UNVISITED_INTRANET:
837 type = UNVISITED_INTRANET; 848 type = UNVISITED_INTRANET;
838 break; 849 break;
839 default: 850 default:
840 DCHECK_EQ(VisitClassifier::VISITED, classifier.type()); 851 DCHECK_EQ(VisitClassifier::VISITED, classifier.type());
841 // We have data for this match, use it. 852 // We have data for this match, use it.
842 match->deletable = true; 853 params->what_you_typed_match.deletable = true;
843 match->description = classifier.url_row().title(); 854 params->what_you_typed_match.description = classifier.url_row().title();
844 RecordAdditionalInfoFromUrlRow(classifier.url_row(), match); 855 RecordAdditionalInfoFromUrlRow(classifier.url_row(),
845 match->description_class = 856 &params->what_you_typed_match);
846 ClassifyDescription(input.text(), match->description); 857 params->what_you_typed_match.description_class = ClassifyDescription(
858 params->input.text(), params->what_you_typed_match.description);
847 if (!classifier.url_row().typed_count()) { 859 if (!classifier.url_row().typed_count()) {
848 // If we reach here, we must be in the second pass, and we must not have 860 // If we reach here, we must be in the second pass, and we must not have
849 // this row's data available during the first pass. That means we 861 // this row's data available during the first pass. That means we
850 // either scored it as WHAT_YOU_TYPED or UNVISITED_INTRANET, and to 862 // either scored it as WHAT_YOU_TYPED or UNVISITED_INTRANET, and to
851 // maintain the ordering between passes consistent, we need to score it 863 // maintain the ordering between passes consistent, we need to score it
852 // the same way here. 864 // the same way here.
853 type = CanFindIntranetURL(db, input) ? 865 type = CanFindIntranetURL(db, params->input) ?
854 UNVISITED_INTRANET : WHAT_YOU_TYPED; 866 UNVISITED_INTRANET : WHAT_YOU_TYPED;
855 } 867 }
856 break; 868 break;
857 } 869 }
858 870
859 const GURL& url = match->destination_url; 871 const GURL& url = params->what_you_typed_match.destination_url;
860 const url::Parsed& parsed = url.parsed_for_possibly_invalid_spec(); 872 const url::Parsed& parsed = url.parsed_for_possibly_invalid_spec();
861 // If the what-you-typed result looks like a single word (which can be 873 // If the what-you-typed result looks like a single word (which can be
862 // interpreted as an intranet address) followed by a pound sign ("#"), 874 // interpreted as an intranet address) followed by a pound sign ("#"),
863 // leave the score for the url-what-you-typed result as is. It will be 875 // leave the score for the url-what-you-typed result as is. It will be
864 // outscored by a search query from the SearchProvider. This test fixes 876 // outscored by a search query from the SearchProvider. This test fixes
865 // cases such as "c#" and "c# foo" where the user has visited an intranet 877 // cases such as "c#" and "c# foo" where the user has visited an intranet
866 // site "c". We want the search-what-you-typed score to beat the 878 // site "c". We want the search-what-you-typed score to beat the
867 // URL-what-you-typed score in this case. Most of the below test tries to 879 // URL-what-you-typed score in this case. Most of the below test tries to
868 // make sure that this code does not trigger if the user did anything to 880 // make sure that this code does not trigger if the user did anything to
869 // indicate the desired match is a URL. For instance, "c/# foo" will not 881 // indicate the desired match is a URL. For instance, "c/# foo" will not
870 // pass the test because that will be classified as input type URL. The 882 // pass the test because that will be classified as input type URL. The
871 // parsed.CountCharactersBefore() in the test looks for the presence of a 883 // parsed.CountCharactersBefore() in the test looks for the presence of a
872 // reference fragment in the URL by checking whether the position differs 884 // reference fragment in the URL by checking whether the position differs
873 // included the delimiter (pound sign) versus not including the delimiter. 885 // included the delimiter (pound sign) versus not including the delimiter.
874 // (One cannot simply check url.ref() because it will not distinguish 886 // (One cannot simply check url.ref() because it will not distinguish
875 // between the input "c" and the input "c#", both of which will have empty 887 // between the input "c" and the input "c#", both of which will have empty
876 // reference fragments.) 888 // reference fragments.)
877 if ((type == UNVISITED_INTRANET) && 889 if ((type == UNVISITED_INTRANET) &&
878 (input.type() != metrics::OmniboxInputType::URL) && 890 (params->input.type() != metrics::OmniboxInputType::URL) &&
879 url.username().empty() && url.password().empty() && url.port().empty() && 891 url.username().empty() && url.password().empty() && url.port().empty() &&
880 (url.path() == "/") && url.query().empty() && 892 (url.path() == "/") && url.query().empty() &&
881 (parsed.CountCharactersBefore(url::Parsed::REF, true) != 893 (parsed.CountCharactersBefore(url::Parsed::REF, true) !=
882 parsed.CountCharactersBefore(url::Parsed::REF, false))) { 894 parsed.CountCharactersBefore(url::Parsed::REF, false))) {
883 return false; 895 return false;
884 } 896 }
885 897
886 match->relevance = CalculateRelevance(type, 0); 898 params->what_you_typed_match.relevance = CalculateRelevance(type, 0);
887 899
888 // If there are any other matches, then don't promote this match here, in 900 // If there are any other matches, then don't promote this match here, in
889 // hopes the caller will be able to inline autocomplete a better suggestion. 901 // hopes the caller will be able to inline autocomplete a better suggestion.
890 // DoAutocomplete() will fall back on this match if inline autocompletion 902 // DoAutocomplete() will fall back on this match if inline autocompletion
891 // fails. This matches how we react to never-visited URL inputs in the non- 903 // fails. This matches how we react to never-visited URL inputs in the non-
892 // intranet case. 904 // intranet case.
893 if (type == UNVISITED_INTRANET && !matches->empty()) 905 if (type == UNVISITED_INTRANET && !matches->empty())
894 return false; 906 return false;
895 907
896 // Put it on the front of the HistoryMatches for redirect culling. 908 // Put it on the front of the HistoryMatches for redirect culling.
(...skipping 19 matching lines...) Expand all
916 net::registry_controlled_domains::GetRegistryLength( 928 net::registry_controlled_domains::GetRegistryLength(
917 host, 929 host,
918 net::registry_controlled_domains::EXCLUDE_UNKNOWN_REGISTRIES, 930 net::registry_controlled_domains::EXCLUDE_UNKNOWN_REGISTRIES,
919 net::registry_controlled_domains::EXCLUDE_PRIVATE_REGISTRIES); 931 net::registry_controlled_domains::EXCLUDE_PRIVATE_REGISTRIES);
920 return registry_length == 0 && db->IsTypedHost(host); 932 return registry_length == 0 && db->IsTypedHost(host);
921 } 933 }
922 934
923 bool HistoryURLProvider::PromoteMatchForInlineAutocomplete( 935 bool HistoryURLProvider::PromoteMatchForInlineAutocomplete(
924 const history::HistoryMatch& match, 936 const history::HistoryMatch& match,
925 HistoryURLProviderParams* params) { 937 HistoryURLProviderParams* params) {
926 // Promote the first match if it's been marked for promotion or typed at least 938 if (!CanPromoteMatchForInlineAutocomplete(match))
927 // n times, where n == 1 for "simple" (host-only) URLs and n == 2 for others.
928 // We set a higher bar for these long URLs because it's less likely that users
929 // will want to visit them again. Even though we don't increment the
930 // typed_count for pasted-in URLs, if the user manually edits the URL or types
931 // some long thing in by hand, we wouldn't want to immediately start
932 // autocompleting it.
933 if (!match.promoted &&
934 (!match.url_info.typed_count() ||
935 ((match.url_info.typed_count() == 1) &&
936 !match.IsHostOnly())))
937 return false; 939 return false;
938 940
939 // In the case where the user has typed "foo.com" and visited (but not typed) 941 // In the case where the user has typed "foo.com" and visited (but not typed)
940 // "foo/", and the input is "foo", we can reach here for "foo.com" during the 942 // "foo/", and the input is "foo", we can reach here for "foo.com" during the
941 // first pass but have the second pass suggest the exact input as a better 943 // first pass but have the second pass suggest the exact input as a better
942 // URL. Since we need both passes to agree, and since during the first pass 944 // URL. Since we need both passes to agree, and since during the first pass
943 // there's no way to know about "foo/", make reaching this point prevent any 945 // there's no way to know about "foo/", make reaching this point prevent any
944 // future pass from suggesting the exact input as a better match. 946 // future pass from suggesting the exact input as a better match.
945 if (params) { 947 params->dont_suggest_exact_input = true;
946 params->dont_suggest_exact_input = true; 948 params->matches.push_back(HistoryMatchToACMatch(
947 AutocompleteMatch ac_match = HistoryMatchToACMatch( 949 *params, match, INLINE_AUTOCOMPLETE,
948 *params, match, INLINE_AUTOCOMPLETE, 950 CalculateRelevance(INLINE_AUTOCOMPLETE, 0)));
949 CalculateRelevance(INLINE_AUTOCOMPLETE, 0));
950 params->matches.push_back(ac_match);
951 }
952 return true; 951 return true;
953 } 952 }
954 953
955 // See if a shorter version of the best match should be created, and if so place
956 // it at the front of |matches|. This can suggest history URLs that are
957 // prefixes of the best match (if they've been visited enough, compared to the
958 // best match), or create host-only suggestions even when they haven't been
959 // visited before: if the user visited http://example.com/asdf once, we'll
960 // suggest http://example.com/ even if they've never been to it.
961 void HistoryURLProvider::PromoteOrCreateShorterSuggestion( 954 void HistoryURLProvider::PromoteOrCreateShorterSuggestion(
962 history::URLDatabase* db, 955 history::URLDatabase* db,
963 const HistoryURLProviderParams& params, 956 const HistoryURLProviderParams& params,
964 bool have_what_you_typed_match, 957 bool have_what_you_typed_match,
965 const AutocompleteMatch& what_you_typed_match,
966 history::HistoryMatches* matches) { 958 history::HistoryMatches* matches) {
967 if (matches->empty()) 959 if (matches->empty())
968 return; // No matches, nothing to do. 960 return; // No matches, nothing to do.
969 961
970 // Determine the base URL from which to search, and whether that URL could 962 // Determine the base URL from which to search, and whether that URL could
971 // itself be added as a match. We can add the base iff it's not "effectively 963 // itself be added as a match. We can add the base iff it's not "effectively
972 // the same" as any "what you typed" match. 964 // the same" as any "what you typed" match.
973 const history::HistoryMatch& match = matches->front(); 965 const history::HistoryMatch& match = matches->front();
974 GURL search_base = ConvertToHostOnly(match, params.input.text()); 966 GURL search_base = ConvertToHostOnly(match, params.input.text());
975 bool can_add_search_base_to_matches = !have_what_you_typed_match; 967 bool can_add_search_base_to_matches = !have_what_you_typed_match;
976 if (search_base.is_empty()) { 968 if (search_base.is_empty()) {
977 // Search from what the user typed when we couldn't reduce the best match 969 // Search from what the user typed when we couldn't reduce the best match
978 // to a host. Careful: use a substring of |match| here, rather than the 970 // to a host. Careful: use a substring of |match| here, rather than the
979 // first match in |params|, because they might have different prefixes. If 971 // first match in |params|, because they might have different prefixes. If
980 // the user typed "google.com", |what_you_typed_match| will hold 972 // the user typed "google.com", params->what_you_typed_match will hold
981 // "http://google.com/", but |match| might begin with 973 // "http://google.com/", but |match| might begin with
982 // "http://www.google.com/". 974 // "http://www.google.com/".
983 // TODO: this should be cleaned up, and is probably incorrect for IDN. 975 // TODO: this should be cleaned up, and is probably incorrect for IDN.
984 std::string new_match = match.url_info.url().possibly_invalid_spec(). 976 std::string new_match = match.url_info.url().possibly_invalid_spec().
985 substr(0, match.input_location + params.input.text().length()); 977 substr(0, match.input_location + params.input.text().length());
986 search_base = GURL(new_match); 978 search_base = GURL(new_match);
987 // TODO(mrossetti): There is a degenerate case where the following may
988 // cause a failure: http://www/~someword/fubar.html. Diagnose.
989 // See: http://crbug.com/50101
990 if (search_base.is_empty()) 979 if (search_base.is_empty())
991 return; // Can't construct a valid URL from which to start a search. 980 return; // Can't construct a valid URL from which to start a search.
992 } else if (!can_add_search_base_to_matches) { 981 } else if (!can_add_search_base_to_matches) {
993 can_add_search_base_to_matches = 982 can_add_search_base_to_matches =
994 (search_base != what_you_typed_match.destination_url); 983 (search_base != params.what_you_typed_match.destination_url);
995 } 984 }
996 if (search_base == match.url_info.url()) 985 if (search_base == match.url_info.url())
997 return; // Couldn't shorten |match|, so no range of URLs to search over. 986 return; // Couldn't shorten |match|, so no range of URLs to search over.
998 987
999 // Search the DB for short URLs between our base and |match|. 988 // Search the DB for short URLs between our base and |match|.
1000 history::URLRow info(search_base); 989 history::URLRow info(search_base);
1001 bool promote = true; 990 bool promote = true;
1002 // A short URL is only worth suggesting if it's been visited at least a third 991 // A short URL is only worth suggesting if it's been visited at least a third
1003 // as often as the longer URL. 992 // as often as the longer URL.
1004 const int min_visit_count = ((match.url_info.visit_count() - 1) / 3) + 1; 993 const int min_visit_count = ((match.url_info.visit_count() - 1) / 3) + 1;
(...skipping 11 matching lines...) Expand all
1016 return; // Couldn't find anything and can't add the search base, bail. 1005 return; // Couldn't find anything and can't add the search base, bail.
1017 1006
1018 // Try to get info on the search base itself. Promote it to the top if the 1007 // Try to get info on the search base itself. Promote it to the top if the
1019 // original best match isn't good enough to autocomplete. 1008 // original best match isn't good enough to autocomplete.
1020 db->GetRowForURL(search_base, &info); 1009 db->GetRowForURL(search_base, &info);
1021 promote = match.url_info.typed_count() <= 1; 1010 promote = match.url_info.typed_count() <= 1;
1022 } 1011 }
1023 1012
1024 // Promote or add the desired URL to the list of matches. 1013 // Promote or add the desired URL to the list of matches.
1025 bool ensure_can_inline = 1014 bool ensure_can_inline =
1026 promote && PromoteMatchForInlineAutocomplete(match, NULL); 1015 promote && CanPromoteMatchForInlineAutocomplete(match);
1027 ensure_can_inline &= CreateOrPromoteMatch(info, match.input_location, 1016 ensure_can_inline &= CreateOrPromoteMatch(info, match.input_location,
1028 match.match_in_scheme, matches, create_shorter_match_, promote); 1017 match.match_in_scheme, matches, create_shorter_match_, promote);
1029 if (ensure_can_inline) 1018 if (ensure_can_inline)
1030 matches->front().promoted = true; 1019 matches->front().promoted = true;
1031 } 1020 }
1032 1021
1033 void HistoryURLProvider::CullPoorMatches( 1022 void HistoryURLProvider::CullPoorMatches(
1034 const HistoryURLProviderParams& params, 1023 const HistoryURLProviderParams& params,
1035 history::HistoryMatches* matches) const { 1024 history::HistoryMatches* matches) const {
1036 const base::Time& threshold(history::AutocompleteAgeThreshold()); 1025 const base::Time& threshold(history::AutocompleteAgeThreshold());
(...skipping 119 matching lines...) Expand 10 before | Expand all | Expand 10 after
1156 AutocompleteMatch::ClassifyLocationInString(base::string16::npos, 0, 1145 AutocompleteMatch::ClassifyLocationInString(base::string16::npos, 0,
1157 match.contents.length(), ACMatchClassification::URL, 1146 match.contents.length(), ACMatchClassification::URL,
1158 &match.contents_class); 1147 &match.contents_class);
1159 } 1148 }
1160 match.description = info.title(); 1149 match.description = info.title();
1161 match.description_class = 1150 match.description_class =
1162 ClassifyDescription(params.input.text(), match.description); 1151 ClassifyDescription(params.input.text(), match.description);
1163 RecordAdditionalInfoFromUrlRow(info, &match); 1152 RecordAdditionalInfoFromUrlRow(info, &match);
1164 return match; 1153 return match;
1165 } 1154 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698