blob: 5ca13ded49549a2fe1ef0daade0b85795291607d [file] [log] [blame]
// Copyright (c) 2009 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/thumbnail_store.h"
#include <string.h>
#include <algorithm>
#include "app/sql/statement.h"
#include "app/sql/transaction.h"
#include "base/basictypes.h"
#include "base/callback.h"
#include "base/file_util.h"
#include "base/md5.h"
#include "base/string_util.h"
#include "base/thread.h"
#include "base/values.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/history/history_notifications.h"
#include "chrome/browser/pref_service.h"
#include "chrome/browser/profile.h"
#include "gfx/codec/jpeg_codec.h"
#include "googleurl/src/gurl.h"
#include "third_party/skia/include/core/SkBitmap.h"
ThumbnailStore::ThumbnailStore()
: cache_(NULL),
hs_(NULL),
url_blacklist_(NULL),
disk_data_loaded_(false) {
}
ThumbnailStore::~ThumbnailStore() {
// Ensure that shutdown was called.
DCHECK(hs_ == NULL);
}
void ThumbnailStore::Init(const FilePath& db_name, Profile* profile) {
// Load thumbnails already in the database.
g_browser_process->file_thread()->message_loop()->PostTask(FROM_HERE,
NewRunnableMethod(this, &ThumbnailStore::InitializeFromDB,
db_name, MessageLoop::current()));
// Take ownership of a reference to the HistoryService.
hs_ = profile->GetHistoryService(Profile::EXPLICIT_ACCESS);
// Store a pointer to a persistent table of blacklisted URLs.
url_blacklist_ = profile->GetPrefs()->
GetMutableDictionary(prefs::kNTPMostVisitedURLsBlacklist);
DCHECK(url_blacklist_);
// Get the list of most visited URLs and redirect information from the
// HistoryService.
most_visited_urls_.reset(new MostVisitedMap);
timer_.Start(base::TimeDelta::FromSeconds(kUpdateIntervalSecs), this,
&ThumbnailStore::UpdateURLData);
UpdateURLData();
// Register to get notified when the history is cleared.
registrar_.Add(this, NotificationType::HISTORY_URLS_DELETED,
Source<Profile>(profile));
}
bool ThumbnailStore::SetPageThumbnail(const GURL& url,
const SkBitmap& thumbnail,
const ThumbnailScore& score,
bool fetch_redirects) {
if (!cache_.get())
return false;
if (!ShouldStoreThumbnailForURL(url) ||
(cache_->find(url) != cache_->end() &&
!ShouldReplaceThumbnailWith((*cache_)[url].score_, score)))
return true;
base::TimeTicks encode_start = base::TimeTicks::Now();
// Encode the SkBitmap to jpeg.
scoped_refptr<RefCountedBytes> jpeg_data = new RefCountedBytes;
SkAutoLockPixels thumbnail_lock(thumbnail);
bool encoded = gfx::JPEGCodec::Encode(
reinterpret_cast<unsigned char*>(thumbnail.getAddr32(0, 0)),
gfx::JPEGCodec::FORMAT_BGRA, thumbnail.width(),
thumbnail.height(),
static_cast<int>(thumbnail.rowBytes()), 90,
&jpeg_data->data);
base::TimeDelta delta = base::TimeTicks::Now() - encode_start;
HISTOGRAM_TIMES("Thumbnail.Encode", delta);
if (!encoded)
return false;
// Update the cache_ with the new thumbnail.
(*cache_)[url] = CacheEntry(jpeg_data, score, true);
// Get redirects for this URL.
if (fetch_redirects) {
hs_->QueryRedirectsTo(url, &consumer_,
NewCallback(this, &ThumbnailStore::OnRedirectsForURLAvailable));
}
return true;
}
bool ThumbnailStore::GetPageThumbnail(
const GURL& url,
RefCountedBytes** data) {
if (!cache_.get() || IsURLBlacklisted(url))
return false;
// Look up the |url| in the redirect list to find the final destination
// which is the key into the |cache_|.
history::RedirectMap::iterator it = redirect_urls_->find(url);
if (it != redirect_urls_->end()) {
// Return the first available thumbnail starting at the end of the
// redirect list.
history::RedirectList::reverse_iterator rit;
for (rit = it->second->data.rbegin();
rit != it->second->data.rend(); ++rit) {
if (cache_->find(*rit) != cache_->end()) {
*data = (*cache_)[*rit].data_.get();
(*data)->AddRef();
return true;
}
}
}
// TODO(meelapshah) bug 14643: check past redirect lists
if (cache_->find(url) == cache_->end())
return false;
*data = (*cache_)[url].data_.get();
(*data)->AddRef();
return true;
}
void ThumbnailStore::Shutdown() {
// We must release our reference to the HistoryService here to prevent
// shutdown issues. Please refer to the comment in HistoryService::Cleanup
// for details.
hs_ = NULL;
// De-register for notifications.
registrar_.RemoveAll();
// Stop the timer to ensure that UpdateURLData is not called during shutdown.
timer_.Stop();
// Write the cache to disk. This will schedule the disk operations to be run
// on the file_thread. Note that Join() does not need to be called with the
// file_thread because when the disk operation is scheduled, it will hold a
// reference to |this| keeping this object alive.
CleanCacheData();
}
void ThumbnailStore::OnRedirectsForURLAvailable(
HistoryService::Handle handle,
GURL url,
bool success,
history::RedirectList* redirects) {
if (!success)
return;
DCHECK(redirect_urls_.get());
// If A -> B -> C is a redirect chain, then this function would be called
// with url=C and redirects = {B, A}. This is entered into the RedirectMap as
// A => {B -> C}.
if (redirects->empty()) {
(*redirect_urls_)[url] = new RefCountedVector<GURL>;
} else {
const GURL start_url = redirects->back();
std::reverse(redirects->begin(), redirects->end() - 1);
*(redirects->end() - 1) = url;
(*redirect_urls_)[start_url] = new RefCountedVector<GURL>(*redirects);
}
}
history::RedirectMap::iterator ThumbnailStore::GetRedirectIteratorForURL(
const GURL& url) const {
for (history::RedirectMap::iterator it = redirect_urls_->begin();
it != redirect_urls_->end(); ++it) {
if (it->first == url ||
(!it->second->data.empty() && it->second->data.back() == url))
return it;
}
return redirect_urls_->end();
}
void ThumbnailStore::Observe(NotificationType type,
const NotificationSource& source,
const NotificationDetails& details) {
if (type.value != NotificationType::HISTORY_URLS_DELETED) {
NOTREACHED();
return;
}
Details<history::URLsDeletedDetails> url_details(details);
// If all history was cleared, clear all of our data and reset the update
// timer.
if (url_details->all_history) {
most_visited_urls_->clear();
redirect_urls_->clear();
cache_->clear();
timer_.Reset();
}
}
void ThumbnailStore::NotifyThumbnailStoreReady() {
NotificationService::current()->Notify(
NotificationType::THUMBNAIL_STORE_READY,
Source<ThumbnailStore>(this),
NotificationService::NoDetails());
}
void ThumbnailStore::UpdateURLData() {
DCHECK(url_blacklist_);
int result_count = ThumbnailStore::kMaxCacheSize + url_blacklist_->size();
hs_->QueryTopURLsAndRedirects(result_count, &consumer_,
NewCallback(this, &ThumbnailStore::OnURLDataAvailable));
}
void ThumbnailStore::OnURLDataAvailable(HistoryService::Handle handle,
bool success,
std::vector<GURL>* urls,
history::RedirectMap* redirects) {
if (!success)
return;
DCHECK(urls);
DCHECK(redirects);
// Each element of |urls| is the start of a redirect chain. When thumbnails
// are stored from TabContents, the tails of the redirect chains are
// associated with the image. Since SetPageThumbnail is called frequently, we
// look up the tail end of each element in |urls| and insert that into the
// MostVisitedMap. This way SetPageThumbnail can more quickly check if a
// given url is in the most visited list.
most_visited_urls_->clear();
for (size_t i = 0; i < urls->size(); ++i) {
history::RedirectMap::iterator it = redirects->find(urls->at(i));
if (it->second->data.empty())
(*most_visited_urls_)[urls->at(i)] = GURL();
else
(*most_visited_urls_)[it->second->data.back()] = urls->at(i);
}
redirect_urls_.reset(new history::RedirectMap(*redirects));
if (IsReady())
NotifyThumbnailStoreReady();
CleanCacheData();
}
void ThumbnailStore::CleanCacheData() {
if (!cache_.get())
return;
scoped_refptr<RefCountedVector<GURL> > urls_to_delete =
new RefCountedVector<GURL>;
Cache* data_to_save = new Cache; // CommitCacheToDB will delete this
// Iterate the cache, storing urls to be deleted and dirty cache entries to
// be written to disk.
for (Cache::iterator cache_it = cache_->begin();
cache_it != cache_->end();) {
history::RedirectMap::iterator redirect_it =
GetRedirectIteratorForURL(cache_it->first);
const GURL* url = redirect_it == redirect_urls_->end() ?
NULL : &redirect_it->first;
// If this URL is blacklisted or not in the most visited list, mark it for
// deletion. Otherwise, if the cache entry is dirty, mark it to be written
// to disk.
if (url == NULL || IsURLBlacklisted(*url) || !IsPopular(*url)) {
urls_to_delete->data.push_back(cache_it->first);
cache_->erase(cache_it++);
} else {
if (cache_it->second.dirty_) {
data_to_save->insert(*cache_it);
cache_it->second.dirty_ = false;
}
++cache_it;
}
}
g_browser_process->file_thread()->message_loop()->PostTask(FROM_HERE,
NewRunnableMethod(this, &ThumbnailStore::CommitCacheToDB,
urls_to_delete, data_to_save));
}
void ThumbnailStore::CommitCacheToDB(
scoped_refptr<RefCountedVector<GURL> > urls_to_delete,
Cache* data) {
scoped_ptr<Cache> data_to_save(data);
if (!db_.is_open())
return;
base::TimeTicks db_start = base::TimeTicks::Now();
sql::Transaction transaction(&db_);
if (!transaction.Begin())
return;
// Delete old thumbnails.
if (urls_to_delete.get()) {
for (std::vector<GURL>::iterator it = urls_to_delete->data.begin();
it != urls_to_delete->data.end(); ++it) {
sql::Statement statement(db_.GetCachedStatement(SQL_FROM_HERE,
"DELETE FROM thumbnails WHERE url=?"));
if (!statement)
return;
statement.BindString(0, it->spec());
if (!statement.Run())
NOTREACHED();
}
}
// Update cached thumbnails.
if (data_to_save.get()) {
for (Cache::iterator it = data_to_save->begin();
it != data_to_save->end(); ++it) {
sql::Statement statement(db_.GetCachedStatement(SQL_FROM_HERE,
"INSERT OR REPLACE INTO thumbnails "
"(url, boring_score, good_clipping, "
"at_top, time_taken, data) "
"VALUES (?,?,?,?,?,?)"));
statement.BindString(0, it->first.spec());
statement.BindDouble(1, it->second.score_.boring_score);
statement.BindBool(2, it->second.score_.good_clipping);
statement.BindBool(3, it->second.score_.at_top);
statement.BindInt64(4,
it->second.score_.time_at_snapshot.ToInternalValue());
statement.BindBlob(5, &it->second.data_->data[0],
static_cast<int>(it->second.data_->data.size()));
if (!statement.Run())
DLOG(WARNING) << "Unable to insert thumbnail for URL";
}
}
transaction.Commit();
base::TimeDelta delta = base::TimeTicks::Now() - db_start;
HISTOGRAM_TIMES("ThumbnailStore.WriteDBToDisk", delta);
}
void ThumbnailStore::InitializeFromDB(const FilePath& db_name,
MessageLoop* cb_loop) {
db_.set_page_size(4096);
db_.set_cache_size(64);
db_.set_exclusive_locking();
if (!db_.Open(db_name))
return;
if (!db_.DoesTableExist("thumbnails")) {
if (!db_.Execute("CREATE TABLE thumbnails ("
"url LONGVARCHAR PRIMARY KEY,"
"boring_score DOUBLE DEFAULT 1.0,"
"good_clipping INTEGER DEFAULT 0,"
"at_top INTEGER DEFAULT 0,"
"time_taken INTEGER DEFAULT 0,"
"data BLOB)"))
return;
}
if (cb_loop)
GetAllThumbnailsFromDisk(cb_loop);
}
void ThumbnailStore::GetAllThumbnailsFromDisk(MessageLoop* cb_loop) {
sql::Statement statement(db_.GetCachedStatement(SQL_FROM_HERE,
"SELECT * FROM thumbnails"));
if (!statement)
return;
Cache* cache = new Cache;
while (statement.Step()) {
// The URL
GURL url(statement.ColumnString(0));
// The score.
ThumbnailScore score(statement.ColumnDouble(1), // Boring score
statement.ColumnBool(2), // Good clipping
statement.ColumnBool(3), // At top
base::Time::FromInternalValue(
statement.ColumnInt64(4))); // Time taken
// The image.
scoped_refptr<RefCountedBytes> data = new RefCountedBytes;
statement.ColumnBlobAsVector(5, &data->data);
(*cache)[url] = CacheEntry(data, score, false);
}
cb_loop->PostTask(FROM_HERE,
NewRunnableMethod(this, &ThumbnailStore::OnDiskDataAvailable, cache));
}
void ThumbnailStore::OnDiskDataAvailable(Cache* cache) {
if (cache)
cache_.reset(cache);
disk_data_loaded_ = true;
if (IsReady())
NotifyThumbnailStoreReady();
}
bool ThumbnailStore::ShouldStoreThumbnailForURL(const GURL& url) const {
if (!cache_.get())
return false;
if (IsURLBlacklisted(url) || cache_->size() >= kMaxCacheSize)
return false;
return IsPopular(url);
}
bool ThumbnailStore::IsURLBlacklisted(const GURL& url) const {
if (url_blacklist_)
return url_blacklist_->HasKey(GetDictionaryKeyForURL(url.spec()));
return false;
}
std::wstring ThumbnailStore::GetDictionaryKeyForURL(
const std::string& url) const {
return ASCIIToWide(MD5String(url));
}
bool ThumbnailStore::IsPopular(const GURL& url) const {
return most_visited_urls_->size() < kMaxCacheSize ||
most_visited_urls_->find(url) != most_visited_urls_->end();
}