top_sites.cc revision 201ade2fbba22bfb27ae029f4d23fca6ded109a0
1// Copyright (c) 2010 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 "chrome/browser/history/top_sites.h"
6
7#include <algorithm>
8#include <set>
9
10#include "app/l10n_util.h"
11#include "base/command_line.h"
12#include "base/logging.h"
13#include "base/md5.h"
14#include "base/string_util.h"
15#include "base/utf_string_conversions.h"
16#include "base/values.h"
17#include "chrome/browser/browser_thread.h"
18#include "chrome/browser/dom_ui/most_visited_handler.h"
19#include "chrome/browser/history/history_backend.h"
20#include "chrome/browser/history/history_notifications.h"
21#include "chrome/browser/history/page_usage_data.h"
22#include "chrome/browser/history/top_sites_backend.h"
23#include "chrome/browser/history/top_sites_cache.h"
24#include "chrome/browser/prefs/pref_service.h"
25#include "chrome/browser/profile.h"
26#include "chrome/browser/tab_contents/navigation_controller.h"
27#include "chrome/browser/tab_contents/navigation_entry.h"
28#include "chrome/common/chrome_switches.h"
29#include "chrome/common/notification_service.h"
30#include "chrome/common/pref_names.h"
31#include "chrome/common/thumbnail_score.h"
32#include "gfx/codec/jpeg_codec.h"
33#include "grit/chromium_strings.h"
34#include "grit/generated_resources.h"
35#include "grit/locale_settings.h"
36#include "third_party/skia/include/core/SkBitmap.h"
37
38namespace history {
39
40// How many top sites to store in the cache.
41static const size_t kTopSitesNumber = 20;
42
43// Max number of temporary images we'll cache. See comment above
44// temp_images_ for details.
45static const size_t kMaxTempTopImages = 8;
46
47static const size_t kTopSitesShown = 8;
48static const int kDaysOfHistory = 90;
49// Time from startup to first HistoryService query.
50static const int64 kUpdateIntervalSecs = 15;
51// Intervals between requests to HistoryService.
52static const int64 kMinUpdateIntervalMinutes = 1;
53static const int64 kMaxUpdateIntervalMinutes = 60;
54
55// IDs of the sites we force into top sites.
56static const int kPrepopulatePageIDs[] =
57    { IDS_CHROME_WELCOME_URL, IDS_THEMES_GALLERY_URL };
58
59// Favicons of the sites we force into top sites.
60static const char kPrepopulateFaviconURLs[][54] =
61    { "chrome://theme/IDR_NEWTAB_CHROME_WELCOME_PAGE_FAVICON",
62      "chrome://theme/IDR_NEWTAB_THEMES_GALLERY_FAVICON" };
63
64static const int kPrepopulateTitleIDs[] =
65    { IDS_NEW_TAB_CHROME_WELCOME_PAGE_TITLE,
66      IDS_NEW_TAB_THEMES_GALLERY_PAGE_TITLE };
67
68namespace {
69
70// HistoryDBTask used during migration of thumbnails from history to top sites.
71// When run on the history thread it collects the top sites and the
72// corresponding thumbnails. When run back on the ui thread it calls into
73// TopSites::FinishHistoryMigration.
74class LoadThumbnailsFromHistoryTask : public HistoryDBTask {
75 public:
76  LoadThumbnailsFromHistoryTask(TopSites* top_sites,
77                                int result_count)
78      : top_sites_(top_sites),
79        result_count_(result_count) {
80    // l10n_util isn't thread safe, so cache for use on the db thread.
81    ignore_urls_.insert(l10n_util::GetStringUTF8(IDS_CHROME_WELCOME_URL));
82    ignore_urls_.insert(l10n_util::GetStringUTF8(IDS_THEMES_GALLERY_URL));
83  }
84
85  virtual bool RunOnDBThread(history::HistoryBackend* backend,
86                             history::HistoryDatabase* db) {
87    // Get the most visited urls.
88    backend->QueryMostVisitedURLsImpl(result_count_,
89                                      kDaysOfHistory,
90                                      &data_.most_visited);
91
92    // And fetch the thumbnails.
93    for (size_t i = 0; i < data_.most_visited.size(); ++i) {
94      const GURL& url = data_.most_visited[i].url;
95      if (ShouldFetchThumbnailFor(url)) {
96        scoped_refptr<RefCountedBytes> data;
97        backend->GetPageThumbnailDirectly(url, &data);
98        data_.url_to_thumbnail_map[url] = data;
99      }
100    }
101    return true;
102  }
103
104  virtual void DoneRunOnMainThread() {
105    top_sites_->FinishHistoryMigration(data_);
106  }
107
108 private:
109  bool ShouldFetchThumbnailFor(const GURL& url) {
110    return ignore_urls_.find(url.spec()) == ignore_urls_.end();
111  }
112
113  // Set of URLs we don't load thumbnails for. This is created on the UI thread
114  // and used on the history thread.
115  std::set<std::string> ignore_urls_;
116
117  scoped_refptr<TopSites> top_sites_;
118
119  // Number of results to request from history.
120  const int result_count_;
121
122  ThumbnailMigration data_;
123
124  DISALLOW_COPY_AND_ASSIGN(LoadThumbnailsFromHistoryTask);
125};
126
127}  // namespace
128
129TopSites::TopSites(Profile* profile)
130    : backend_(new TopSitesBackend()),
131      cache_(new TopSitesCache()),
132      thread_safe_cache_(new TopSitesCache()),
133      profile_(profile),
134      last_num_urls_changed_(0),
135      blacklist_(NULL),
136      pinned_urls_(NULL),
137      history_state_(HISTORY_LOADING),
138      top_sites_state_(TOP_SITES_LOADING),
139      loaded_(false) {
140  if (!profile_)
141    return;
142
143  if (NotificationService::current()) {
144    registrar_.Add(this, NotificationType::HISTORY_URLS_DELETED,
145                   Source<Profile>(profile_));
146    registrar_.Add(this, NotificationType::NAV_ENTRY_COMMITTED,
147                   NotificationService::AllSources());
148  }
149
150  blacklist_ = profile_->GetPrefs()->
151      GetMutableDictionary(prefs::kNTPMostVisitedURLsBlacklist);
152  pinned_urls_ = profile_->GetPrefs()->
153      GetMutableDictionary(prefs::kNTPMostVisitedPinnedURLs);
154}
155
156// static
157bool TopSites::IsEnabled() {
158  std::string switch_value =
159      CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
160          switches::kEnableTopSites);
161  return switch_value.empty() || switch_value == "true";
162}
163
164void TopSites::Init(const FilePath& db_name) {
165  backend_->Init(db_name);
166  backend_->GetMostVisitedThumbnails(
167      &cancelable_consumer_,
168      NewCallback(this, &TopSites::OnGotMostVisitedThumbnails));
169
170  // History may have already finished loading by the time we're created.
171  HistoryService* history = profile_->GetHistoryServiceWithoutCreating();
172  if (history && history->backend_loaded()) {
173    if (history->needs_top_sites_migration())
174      MigrateFromHistory();
175    else
176      history_state_ = HISTORY_LOADED;
177  }
178}
179
180bool TopSites::SetPageThumbnail(const GURL& url,
181                                const SkBitmap& thumbnail,
182                                const ThumbnailScore& score) {
183  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
184
185  if (!loaded_) {
186    // TODO(sky): I need to cache these and apply them after the load
187    // completes.
188    return false;
189  }
190
191  bool add_temp_thumbnail = false;
192  if (!cache_->IsKnownURL(url)) {
193    if (cache_->top_sites().size() < kTopSitesNumber) {
194      add_temp_thumbnail = true;
195    } else {
196      return false;  // This URL is not known to us.
197    }
198  }
199
200  if (!HistoryService::CanAddURL(url))
201    return false;  // It's not a real webpage.
202
203  scoped_refptr<RefCountedBytes> thumbnail_data;
204  if (!EncodeBitmap(thumbnail, &thumbnail_data))
205    return false;
206
207  if (add_temp_thumbnail) {
208    // Always remove the existing entry and then add it back. That way if we end
209    // up with too many temp thumbnails we'll prune the oldest first.
210    RemoveTemporaryThumbnailByURL(url);
211    AddTemporaryThumbnail(url, thumbnail_data, score);
212    return true;
213  }
214
215  return SetPageThumbnailEncoded(url, thumbnail_data, score);
216}
217
218void TopSites::GetMostVisitedURLs(CancelableRequestConsumer* consumer,
219                                  GetTopSitesCallback* callback) {
220  // WARNING: this may be invoked on any thread.
221  scoped_refptr<CancelableRequest<GetTopSitesCallback> > request(
222      new CancelableRequest<GetTopSitesCallback>(callback));
223  // This ensures cancelation of requests when either the consumer or the
224  // provider is deleted. Deletion of requests is also guaranteed.
225  AddRequest(request, consumer);
226  MostVisitedURLList filtered_urls;
227  {
228    AutoLock lock(lock_);
229    if (!loaded_) {
230      // A request came in before we finished loading. Put the request in
231      // pending_callbacks_ and we'll notify it when we finish loading.
232      pending_callbacks_.insert(request);
233      return;
234    }
235
236    filtered_urls = thread_safe_cache_->top_sites();
237  }
238  request->ForwardResult(GetTopSitesCallback::TupleType(filtered_urls));
239}
240
241bool TopSites::GetPageThumbnail(const GURL& url,
242                                scoped_refptr<RefCountedBytes>* bytes) {
243  // WARNING: this may be invoked on any thread.
244  AutoLock lock(lock_);
245  return thread_safe_cache_->GetPageThumbnail(url, bytes);
246}
247
248// Returns the index of |url| in |urls|, or -1 if not found.
249static int IndexOf(const MostVisitedURLList& urls, const GURL& url) {
250  for (size_t i = 0; i < urls.size(); i++) {
251    if (urls[i].url == url)
252      return i;
253  }
254  return -1;
255}
256
257void TopSites::MigrateFromHistory() {
258  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
259  DCHECK_EQ(history_state_, HISTORY_LOADING);
260
261  history_state_ = HISTORY_MIGRATING;
262  profile_->GetHistoryService(Profile::EXPLICIT_ACCESS)->ScheduleDBTask(
263      new LoadThumbnailsFromHistoryTask(
264          this,
265          num_results_to_request_from_history()),
266      &cancelable_consumer_);
267  MigratePinnedURLs();
268}
269
270void TopSites::FinishHistoryMigration(const ThumbnailMigration& data) {
271  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
272  DCHECK_EQ(history_state_, HISTORY_MIGRATING);
273
274  history_state_ = HISTORY_LOADED;
275
276  SetTopSites(data.most_visited);
277
278  for (size_t i = 0; i < data.most_visited.size(); ++i) {
279    URLToThumbnailMap::const_iterator image_i =
280        data.url_to_thumbnail_map.find(data.most_visited[i].url);
281    if (image_i != data.url_to_thumbnail_map.end()) {
282      SetPageThumbnailEncoded(data.most_visited[i].url,
283                              image_i->second,
284                              ThumbnailScore());
285    }
286  }
287
288  MoveStateToLoaded();
289
290  ResetThreadSafeImageCache();
291
292  // We've scheduled all the thumbnails and top sites to be written to the top
293  // sites db, but it hasn't happened yet. Schedule a request on the db thread
294  // that notifies us when done. When done we'll know everything was written and
295  // we can tell history to finish its part of migration.
296  backend_->DoEmptyRequest(
297      &cancelable_consumer_,
298      NewCallback(this, &TopSites::OnHistoryMigrationWrittenToDisk));
299}
300
301void TopSites::HistoryLoaded() {
302  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
303  DCHECK_NE(history_state_, HISTORY_LOADED);
304
305  if (history_state_ != HISTORY_MIGRATING) {
306    // No migration from history is needed.
307    history_state_ = HISTORY_LOADED;
308    if (top_sites_state_ == TOP_SITES_LOADED_WAITING_FOR_HISTORY) {
309      // TopSites thought it needed migration, but it really didn't. This
310      // typically happens the first time a profile is run with Top Sites
311      // enabled
312      SetTopSites(MostVisitedURLList());
313      MoveStateToLoaded();
314    }
315  }
316}
317
318bool TopSites::HasBlacklistedItems() const {
319  return !blacklist_->empty();
320}
321
322void TopSites::AddBlacklistedURL(const GURL& url) {
323  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
324
325  RemovePinnedURL(url);
326  Value* dummy = Value::CreateNullValue();
327  blacklist_->SetWithoutPathExpansion(GetURLHash(url), dummy);
328
329  ResetThreadSafeCache();
330}
331
332void TopSites::RemoveBlacklistedURL(const GURL& url) {
333  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
334  blacklist_->RemoveWithoutPathExpansion(GetURLHash(url), NULL);
335  ResetThreadSafeCache();
336}
337
338bool TopSites::IsBlacklisted(const GURL& url) {
339  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
340  return blacklist_->HasKey(GetURLHash(url));
341}
342
343void TopSites::ClearBlacklistedURLs() {
344  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
345  blacklist_->Clear();
346  ResetThreadSafeCache();
347}
348
349void TopSites::AddPinnedURL(const GURL& url, size_t pinned_index) {
350  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
351
352  GURL old;
353  if (GetPinnedURLAtIndex(pinned_index, &old))
354    RemovePinnedURL(old);
355
356  if (IsURLPinned(url))
357    RemovePinnedURL(url);
358
359  Value* index = Value::CreateIntegerValue(pinned_index);
360  pinned_urls_->SetWithoutPathExpansion(GetURLString(url), index);
361
362  ResetThreadSafeCache();
363}
364
365bool TopSites::IsURLPinned(const GURL& url) {
366  int tmp;
367  return pinned_urls_->GetIntegerWithoutPathExpansion(GetURLString(url), &tmp);
368}
369
370void TopSites::RemovePinnedURL(const GURL& url) {
371  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
372
373  pinned_urls_->RemoveWithoutPathExpansion(GetURLString(url), NULL);
374
375  ResetThreadSafeCache();
376}
377
378bool TopSites::GetPinnedURLAtIndex(size_t index, GURL* url) {
379  for (DictionaryValue::key_iterator it = pinned_urls_->begin_keys();
380       it != pinned_urls_->end_keys(); ++it) {
381    int current_index;
382    if (pinned_urls_->GetIntegerWithoutPathExpansion(*it, &current_index)) {
383      if (static_cast<size_t>(current_index) == index) {
384        *url = GURL(*it);
385        return true;
386      }
387    }
388  }
389  return false;
390}
391
392void TopSites::Shutdown() {
393  profile_ = NULL;
394  // Cancel all requests so that the service doesn't callback to us after we've
395  // invoked Shutdown (this could happen if we have a pending request and
396  // Shutdown is invoked).
397  cancelable_consumer_.CancelAllRequests();
398  backend_->Shutdown();
399}
400
401// static
402void TopSites::DiffMostVisited(const MostVisitedURLList& old_list,
403                               const MostVisitedURLList& new_list,
404                               TopSitesDelta* delta) {
405  // Add all the old URLs for quick lookup. This maps URLs to the corresponding
406  // index in the input.
407  std::map<GURL, size_t> all_old_urls;
408  for (size_t i = 0; i < old_list.size(); i++)
409    all_old_urls[old_list[i].url] = i;
410
411  // Check all the URLs in the new set to see which ones are new or just moved.
412  // When we find a match in the old set, we'll reset its index to our special
413  // marker. This allows us to quickly identify the deleted ones in a later
414  // pass.
415  const size_t kAlreadyFoundMarker = static_cast<size_t>(-1);
416  for (size_t i = 0; i < new_list.size(); i++) {
417    std::map<GURL, size_t>::iterator found = all_old_urls.find(new_list[i].url);
418    if (found == all_old_urls.end()) {
419      MostVisitedURLWithRank added;
420      added.url = new_list[i];
421      added.rank = i;
422      delta->added.push_back(added);
423    } else {
424      if (found->second != i) {
425        MostVisitedURLWithRank moved;
426        moved.url = new_list[i];
427        moved.rank = i;
428        delta->moved.push_back(moved);
429      }
430      found->second = kAlreadyFoundMarker;
431    }
432  }
433
434  // Any member without the special marker in the all_old_urls list means that
435  // there wasn't a "new" URL that mapped to it, so it was deleted.
436  for (std::map<GURL, size_t>::const_iterator i = all_old_urls.begin();
437       i != all_old_urls.end(); ++i) {
438    if (i->second != kAlreadyFoundMarker)
439      delta->deleted.push_back(old_list[i->second]);
440  }
441}
442
443CancelableRequestProvider::Handle TopSites::StartQueryForMostVisited() {
444  DCHECK(loaded_);
445  if (!profile_)
446    return NULL;
447
448  HistoryService* hs = profile_->GetHistoryService(Profile::EXPLICIT_ACCESS);
449  // |hs| may be null during unit tests.
450  if (hs) {
451    return hs->QueryMostVisitedURLs(
452        num_results_to_request_from_history(),
453        kDaysOfHistory,
454        &cancelable_consumer_,
455        NewCallback(this, &TopSites::OnTopSitesAvailableFromHistory));
456  }
457  return NULL;
458}
459
460TopSites::~TopSites() {
461}
462
463bool TopSites::SetPageThumbnailNoDB(const GURL& url,
464                                    const RefCountedBytes* thumbnail_data,
465                                    const ThumbnailScore& score) {
466  // This should only be invoked when we know about the url.
467  DCHECK(cache_->IsKnownURL(url));
468
469  const MostVisitedURL& most_visited =
470      cache_->top_sites()[cache_->GetURLIndex(url)];
471  Images* image = cache_->GetImage(url);
472
473  // When comparing the thumbnail scores, we need to take into account the
474  // redirect hops, which are not generated when the thumbnail is because the
475  // redirects weren't known. We fill that in here since we know the redirects.
476  ThumbnailScore new_score_with_redirects(score);
477  new_score_with_redirects.redirect_hops_from_dest =
478      GetRedirectDistanceForURL(most_visited, url);
479
480  if (!ShouldReplaceThumbnailWith(image->thumbnail_score,
481                                  new_score_with_redirects) &&
482      image->thumbnail.get())
483    return false;  // The one we already have is better.
484
485  image->thumbnail = const_cast<RefCountedBytes*>(thumbnail_data);
486  image->thumbnail_score = new_score_with_redirects;
487
488  ResetThreadSafeImageCache();
489  return true;
490}
491
492bool TopSites::SetPageThumbnailEncoded(const GURL& url,
493                                       const RefCountedBytes* thumbnail,
494                                       const ThumbnailScore& score) {
495  if (!SetPageThumbnailNoDB(url, thumbnail, score))
496    return false;
497
498  // Update the database.
499  if (!cache_->IsKnownURL(url))
500    return false;
501
502  size_t index = cache_->GetURLIndex(url);
503  const MostVisitedURL& most_visited = cache_->top_sites()[index];
504  backend_->SetPageThumbnail(most_visited,
505                             index,
506                             *(cache_->GetImage(most_visited.url)));
507  return true;
508}
509
510// static
511bool TopSites::EncodeBitmap(const SkBitmap& bitmap,
512                            scoped_refptr<RefCountedBytes>* bytes) {
513  *bytes = new RefCountedBytes();
514  SkAutoLockPixels bitmap_lock(bitmap);
515  std::vector<unsigned char> data;
516  if (!gfx::JPEGCodec::Encode(
517          reinterpret_cast<unsigned char*>(bitmap.getAddr32(0, 0)),
518          gfx::JPEGCodec::FORMAT_BGRA, bitmap.width(),
519          bitmap.height(),
520          static_cast<int>(bitmap.rowBytes()), 90,
521          &data)) {
522    return false;
523  }
524  // As we're going to cache this data, make sure the vector is only as big as
525  // it needs to be.
526  (*bytes)->data = data;
527  return true;
528}
529
530void TopSites::RemoveTemporaryThumbnailByURL(const GURL& url) {
531  for (TempImages::iterator i = temp_images_.begin(); i != temp_images_.end();
532       ++i) {
533    if (i->first == url) {
534      temp_images_.erase(i);
535      return;
536    }
537  }
538}
539
540void TopSites::AddTemporaryThumbnail(const GURL& url,
541                                     const RefCountedBytes* thumbnail,
542                                     const ThumbnailScore& score) {
543  if (temp_images_.size() == kMaxTempTopImages)
544    temp_images_.erase(temp_images_.begin());
545
546  TempImage image;
547  image.first = url;
548  image.second.thumbnail = const_cast<RefCountedBytes*>(thumbnail);
549  image.second.thumbnail_score = score;
550  temp_images_.push_back(image);
551}
552
553void TopSites::TimerFired() {
554  StartQueryForMostVisited();
555}
556
557// static
558int TopSites::GetRedirectDistanceForURL(const MostVisitedURL& most_visited,
559                                        const GURL& url) {
560  for (size_t i = 0; i < most_visited.redirects.size(); i++) {
561    if (most_visited.redirects[i] == url)
562      return static_cast<int>(most_visited.redirects.size() - i - 1);
563  }
564  NOTREACHED() << "URL should always be found.";
565  return 0;
566}
567
568// static
569MostVisitedURLList TopSites::GetPrepopulatePages() {
570  MostVisitedURLList urls;
571  urls.resize(arraysize(kPrepopulatePageIDs));
572  for (size_t i = 0; i < arraysize(kPrepopulatePageIDs); ++i) {
573    MostVisitedURL& url = urls[i];
574    url.url = GURL(l10n_util::GetStringUTF8(kPrepopulatePageIDs[i]));
575    url.redirects.push_back(url.url);
576    url.favicon_url = GURL(kPrepopulateFaviconURLs[i]);
577    url.title = l10n_util::GetStringUTF16(kPrepopulateTitleIDs[i]);
578  }
579  return urls;
580}
581
582// static
583bool TopSites::AddPrepopulatedPages(MostVisitedURLList* urls) {
584  bool added = false;
585  MostVisitedURLList prepopulate_urls = GetPrepopulatePages();
586  for (size_t i = 0; i < prepopulate_urls.size(); ++i) {
587    if (urls->size() < kTopSitesNumber &&
588        IndexOf(*urls, prepopulate_urls[i].url) == -1) {
589      urls->push_back(prepopulate_urls[i]);
590      added = true;
591    }
592  }
593  return added;
594}
595
596void TopSites::MigratePinnedURLs() {
597  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
598
599  std::map<GURL, size_t> tmp_map;
600  for (DictionaryValue::key_iterator it = pinned_urls_->begin_keys();
601       it != pinned_urls_->end_keys(); ++it) {
602    Value* value;
603    if (!pinned_urls_->GetWithoutPathExpansion(*it, &value))
604      continue;
605
606    if (value->IsType(DictionaryValue::TYPE_DICTIONARY)) {
607      DictionaryValue* dict = static_cast<DictionaryValue*>(value);
608      std::string url_string;
609      int index;
610      if (dict->GetString("url", &url_string) &&
611          dict->GetInteger("index", &index))
612        tmp_map[GURL(url_string)] = index;
613    }
614  }
615  pinned_urls_->Clear();
616  for (std::map<GURL, size_t>::iterator it = tmp_map.begin();
617       it != tmp_map.end(); ++it)
618    AddPinnedURL(it->first, it->second);
619}
620
621void TopSites::ApplyBlacklistAndPinnedURLs(const MostVisitedURLList& urls,
622                                           MostVisitedURLList* out) {
623  MostVisitedURLList urls_copy;
624  for (size_t i = 0; i < urls.size(); i++) {
625    if (!IsBlacklisted(urls[i].url))
626      urls_copy.push_back(urls[i]);
627  }
628
629  for (size_t pinned_index = 0; pinned_index < kTopSitesShown; pinned_index++) {
630    GURL url;
631    bool found = GetPinnedURLAtIndex(pinned_index, &url);
632    if (!found)
633      continue;
634
635    DCHECK(!url.is_empty());
636    int cur_index = IndexOf(urls_copy, url);
637    MostVisitedURL tmp;
638    if (cur_index < 0) {
639      // Pinned URL not in urls.
640      tmp.url = url;
641    } else {
642      tmp = urls_copy[cur_index];
643      urls_copy.erase(urls_copy.begin() + cur_index);
644    }
645    if (pinned_index > out->size())
646      out->resize(pinned_index);  // Add empty URLs as fillers.
647    out->insert(out->begin() + pinned_index, tmp);
648  }
649
650  // Add non-pinned URLs in the empty spots.
651  size_t current_url = 0;  // Index into the remaining URLs in urls_copy.
652  for (size_t i = 0; i < kTopSitesShown && current_url < urls_copy.size();
653       i++) {
654    if (i == out->size()) {
655      out->push_back(urls_copy[current_url]);
656      current_url++;
657    } else if (i < out->size()) {
658      if ((*out)[i].url.is_empty()) {
659        // Replace the filler
660        (*out)[i] = urls_copy[current_url];
661        current_url++;
662      }
663    } else {
664      NOTREACHED();
665    }
666  }
667}
668
669std::string TopSites::GetURLString(const GURL& url) {
670  return cache_->GetCanonicalURL(url).spec();
671}
672
673std::string TopSites::GetURLHash(const GURL& url) {
674  // We don't use canonical URLs here to be able to blacklist only one of
675  // the two 'duplicate' sites, e.g. 'gmail.com' and 'mail.google.com'.
676  return MD5String(url.spec());
677}
678
679base::TimeDelta TopSites::GetUpdateDelay() {
680  if (cache_->top_sites().size() <= arraysize(kPrepopulateTitleIDs))
681    return base::TimeDelta::FromSeconds(30);
682
683  int64 range = kMaxUpdateIntervalMinutes - kMinUpdateIntervalMinutes;
684  int64 minutes = kMaxUpdateIntervalMinutes -
685      last_num_urls_changed_ * range / cache_->top_sites().size();
686  return base::TimeDelta::FromMinutes(minutes);
687}
688
689// static
690void TopSites::ProcessPendingCallbacks(
691    const PendingCallbackSet& pending_callbacks,
692    const MostVisitedURLList& urls) {
693  PendingCallbackSet::const_iterator i;
694  for (i = pending_callbacks.begin();
695       i != pending_callbacks.end(); ++i) {
696    scoped_refptr<CancelableRequest<GetTopSitesCallback> > request = *i;
697    if (!request->canceled())
698      request->ForwardResult(GetTopSitesCallback::TupleType(urls));
699  }
700}
701
702void TopSites::Observe(NotificationType type,
703                       const NotificationSource& source,
704                       const NotificationDetails& details) {
705  if (!loaded_)
706    return;
707
708  if (type == NotificationType::HISTORY_URLS_DELETED) {
709    Details<history::URLsDeletedDetails> deleted_details(details);
710    if (deleted_details->all_history) {
711      SetTopSites(MostVisitedURLList());
712      backend_->ResetDatabase();
713    } else {
714      std::set<size_t> indices_to_delete;  // Indices into top_sites_.
715      for (std::set<GURL>::iterator i = deleted_details->urls.begin();
716           i != deleted_details->urls.end(); ++i) {
717        if (cache_->IsKnownURL(*i))
718          indices_to_delete.insert(cache_->GetURLIndex(*i));
719      }
720
721      if (indices_to_delete.empty())
722        return;
723
724      MostVisitedURLList new_top_sites(cache_->top_sites());
725      for (std::set<size_t>::reverse_iterator i = indices_to_delete.rbegin();
726           i != indices_to_delete.rend(); i++) {
727        size_t index = *i;
728        RemovePinnedURL(new_top_sites[index].url);
729        new_top_sites.erase(new_top_sites.begin() + index);
730      }
731      SetTopSites(new_top_sites);
732    }
733    StartQueryForMostVisited();
734  } else if (type == NotificationType::NAV_ENTRY_COMMITTED) {
735    if (cache_->top_sites().size() < kTopSitesNumber) {
736      NavigationController::LoadCommittedDetails* load_details =
737          Details<NavigationController::LoadCommittedDetails>(details).ptr();
738      if (!load_details)
739        return;
740      const GURL& url = load_details->entry->url();
741      if (!cache_->IsKnownURL(url) && HistoryService::CanAddURL(url)) {
742        // To avoid slamming history we throttle requests when the url updates.
743        // To do otherwise negatively impacts perf tests.
744        RestartQueryForTopSitesTimer(GetUpdateDelay());
745      }
746    }
747  }
748}
749
750void TopSites::SetTopSites(const MostVisitedURLList& new_top_sites) {
751  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
752
753  MostVisitedURLList top_sites(new_top_sites);
754  AddPrepopulatedPages(&top_sites);
755
756  TopSitesDelta delta;
757  DiffMostVisited(cache_->top_sites(), top_sites, &delta);
758  if (!delta.deleted.empty() || !delta.added.empty() || !delta.moved.empty())
759    backend_->UpdateTopSites(delta);
760
761  last_num_urls_changed_ = delta.added.size() + delta.moved.size();
762
763  // We always do the following steps (setting top sites in cache, and resetting
764  // thread safe cache ...) as this method is invoked during startup at which
765  // point the caches haven't been updated yet.
766  cache_->SetTopSites(top_sites);
767
768  // See if we have any tmp thumbnails for the new sites.
769  if (!temp_images_.empty()) {
770    for (size_t i = 0; i < top_sites.size(); ++i) {
771      const MostVisitedURL& mv = top_sites[i];
772      GURL canonical_url = cache_->GetCanonicalURL(mv.url);
773      // At the time we get the thumbnail redirects aren't known, so we have to
774      // iterate through all the images.
775      for (TempImages::iterator it = temp_images_.begin();
776           it != temp_images_.end(); ++it) {
777        if (canonical_url == cache_->GetCanonicalURL(it->first)) {
778          SetPageThumbnailEncoded(mv.url,
779                                  it->second.thumbnail,
780                                  it->second.thumbnail_score);
781          temp_images_.erase(it);
782          break;
783        }
784      }
785    }
786  }
787
788  if (top_sites.size() >= kTopSitesNumber)
789    temp_images_.clear();
790
791  ResetThreadSafeCache();
792  ResetThreadSafeImageCache();
793
794  // Restart the timer that queries history for top sites. This is done to
795  // ensure we stay in sync with history.
796  RestartQueryForTopSitesTimer(GetUpdateDelay());
797}
798
799int TopSites::num_results_to_request_from_history() const {
800  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
801
802  return kTopSitesNumber + blacklist_->size();
803}
804
805void TopSites::MoveStateToLoaded() {
806  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
807
808  MostVisitedURLList filtered_urls;
809  PendingCallbackSet pending_callbacks;
810  {
811    AutoLock lock(lock_);
812
813    if (loaded_)
814      return;  // Don't do anything if we're already loaded.
815    loaded_ = true;
816
817    // Now that we're loaded we can service the queued up callbacks. Copy them
818    // here and service them outside the lock.
819    if (!pending_callbacks_.empty()) {
820      filtered_urls = thread_safe_cache_->top_sites();
821      pending_callbacks.swap(pending_callbacks_);
822    }
823  }
824
825  ProcessPendingCallbacks(pending_callbacks, filtered_urls);
826
827  NotificationService::current()->Notify(NotificationType::TOP_SITES_LOADED,
828                                         Source<Profile>(profile_),
829                                         Details<TopSites>(this));
830}
831
832void TopSites::ResetThreadSafeCache() {
833  AutoLock lock(lock_);
834  MostVisitedURLList cached;
835  ApplyBlacklistAndPinnedURLs(cache_->top_sites(), &cached);
836  thread_safe_cache_->SetTopSites(cached);
837}
838
839void TopSites::ResetThreadSafeImageCache() {
840  AutoLock lock(lock_);
841  thread_safe_cache_->SetThumbnails(cache_->images());
842  thread_safe_cache_->RemoveUnreferencedThumbnails();
843}
844
845void TopSites::RestartQueryForTopSitesTimer(base::TimeDelta delta) {
846  if (timer_.IsRunning() && ((timer_start_time_ + timer_.GetCurrentDelay()) <
847                             (base::TimeTicks::Now() + delta))) {
848    return;
849  }
850
851  timer_start_time_ = base::TimeTicks::Now();
852  timer_.Stop();
853  timer_.Start(delta, this, &TopSites::TimerFired);
854}
855
856void TopSites::OnHistoryMigrationWrittenToDisk(TopSitesBackend::Handle handle) {
857  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
858
859  if (!profile_)
860    return;
861
862  HistoryService* history =
863      profile_->GetHistoryService(Profile::EXPLICIT_ACCESS);
864  if (history)
865    history->OnTopSitesReady();
866}
867
868void TopSites::OnGotMostVisitedThumbnails(
869    CancelableRequestProvider::Handle handle,
870    scoped_refptr<MostVisitedThumbnails> data,
871    bool may_need_history_migration) {
872  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
873  DCHECK_EQ(top_sites_state_, TOP_SITES_LOADING);
874
875  if (!may_need_history_migration) {
876    top_sites_state_ = TOP_SITES_LOADED;
877
878    // Set the top sites directly in the cache so that SetTopSites diffs
879    // correctly.
880    cache_->SetTopSites(data->most_visited);
881    SetTopSites(data->most_visited);
882    cache_->SetThumbnails(data->url_to_images_map);
883
884    ResetThreadSafeImageCache();
885
886    MoveStateToLoaded();
887
888    // Start a timer that refreshes top sites from history.
889    RestartQueryForTopSitesTimer(
890        base::TimeDelta::FromSeconds(kUpdateIntervalSecs));
891  } else {
892    // The top sites file didn't exist or is the wrong version. We need to wait
893    // for history to finish loading to know if we really needed to migrate.
894    if (history_state_ == HISTORY_LOADED) {
895      top_sites_state_ = TOP_SITES_LOADED;
896      SetTopSites(MostVisitedURLList());
897      MoveStateToLoaded();
898    } else {
899      top_sites_state_ = TOP_SITES_LOADED_WAITING_FOR_HISTORY;
900      // Ask for history just in case it hasn't been loaded yet. When history
901      // finishes loading we'll do migration and/or move to loaded.
902      profile_->GetHistoryService(Profile::EXPLICIT_ACCESS);
903    }
904  }
905}
906
907void TopSites::OnTopSitesAvailableFromHistory(
908    CancelableRequestProvider::Handle handle,
909    MostVisitedURLList pages) {
910  SetTopSites(pages);
911
912  // Used only in testing.
913  NotificationService::current()->Notify(
914      NotificationType::TOP_SITES_UPDATED,
915      Source<TopSites>(this),
916      Details<CancelableRequestProvider::Handle>(&handle));
917}
918
919}  // namespace history
920