blob: d58bf5c6f68d7243367e90121d20931622be0c45 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/enhanced_bookmarks/bookmark_server_cluster_service.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/memory/scoped_ptr.h"
#include "base/prefs/pref_service.h"
#include "base/values.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/enhanced_bookmarks/enhanced_bookmark_model.h"
#include "components/enhanced_bookmarks/enhanced_bookmark_utils.h"
#include "components/enhanced_bookmarks/pref_names.h"
#include "components/enhanced_bookmarks/proto/cluster.pb.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/signin/core/browser/signin_manager.h"
#include "components/sync_driver/sync_service.h"
#include "net/base/url_util.h"
#include "net/url_request/url_fetcher.h"
#include "net/url_request/url_request_context_getter.h"
using bookmarks::BookmarkNode;
namespace {
const char kClusterUrl[] = "https://www.google.com/stars/cluster";
const int kPrefServiceVersion = 1;
const char kPrefServiceVersionKey[] = "version";
const char kPrefServiceDataKey[] = "data";
const char kAuthIdKey[] = "auth_id";
} // namespace
namespace enhanced_bookmarks {
BookmarkServerClusterService::BookmarkServerClusterService(
const std::string& application_language_code,
scoped_refptr<net::URLRequestContextGetter> request_context_getter,
ProfileOAuth2TokenService* token_service,
SigninManagerBase* signin_manager,
enhanced_bookmarks::EnhancedBookmarkModel* enhanced_bookmark_model,
sync_driver::SyncService* sync_service,
PrefService* pref_service)
: BookmarkServerService(request_context_getter,
token_service,
signin_manager,
enhanced_bookmark_model),
application_language_code_(application_language_code),
sync_service_(sync_service),
pref_service_(pref_service),
sync_refresh_skipped_(false),
refreshes_needed_(0) {
LoadModel();
if (model_->loaded())
TriggerTokenRequest(false);
GetSigninManager()->AddObserver(this);
if (sync_service_)
sync_service_->AddObserver(this);
}
BookmarkServerClusterService::~BookmarkServerClusterService() {
}
void BookmarkServerClusterService::Shutdown() {
if (sync_service_)
sync_service_->RemoveObserver(this);
GetSigninManager()->RemoveObserver(this);
}
const std::vector<const BookmarkNode*>
BookmarkServerClusterService::BookmarksForClusterNamed(
const std::string& cluster_name) const {
std::vector<const BookmarkNode*> results;
ClusterMap::const_iterator cluster_it = cluster_data_.find(cluster_name);
if (cluster_it == cluster_data_.end())
return results;
for (auto& star_id : cluster_it->second) {
const BookmarkNode* bookmark = BookmarkForRemoteId(star_id);
if (bookmark)
results.push_back(bookmark);
}
return results;
}
const std::vector<std::string>
BookmarkServerClusterService::ClustersForBookmark(
const BookmarkNode* bookmark) const {
const std::string& star_id = RemoteIDForBookmark(bookmark);
// TODO(noyau): if this turns out to be a perf bottleneck this may be improved
// by storing a reverse map from id to cluster.
std::vector<std::string> clusters;
for (auto& pair : cluster_data_) {
const std::vector<std::string>& stars_ids = pair.second;
if (std::find(stars_ids.begin(), stars_ids.end(), star_id) !=
stars_ids.end())
clusters.push_back(pair.first);
}
return clusters;
}
const std::vector<std::string> BookmarkServerClusterService::GetClusters()
const {
std::vector<std::string> cluster_names;
for (auto& pair : cluster_data_) {
for (auto& star_id : pair.second) {
const BookmarkNode* bookmark = BookmarkForRemoteId(star_id);
if (bookmark) {
// Only add clusters that have children.
cluster_names.push_back(pair.first);
break;
}
}
}
return cluster_names;
}
void BookmarkServerClusterService::AddObserver(
enhanced_bookmarks::BookmarkServerServiceObserver* observer) {
BookmarkServerService::AddObserver(observer);
if (sync_refresh_skipped_) {
TriggerTokenRequest(false);
sync_refresh_skipped_ = true;
}
}
// static
void BookmarkServerClusterService::RegisterPrefs(
user_prefs::PrefRegistrySyncable* registry) {
registry->RegisterDictionaryPref(prefs::kBookmarkClusters);
}
scoped_ptr<net::URLFetcher> BookmarkServerClusterService::CreateFetcher() {
// Add the necessary arguments to the URI.
GURL url(kClusterUrl);
url = net::AppendQueryParameter(url, "output", "proto");
// Append language.
if (!application_language_code_.empty())
url = net::AppendQueryParameter(url, "hl", application_language_code_);
url = net::AppendQueryParameter(url, "v", model_->GetVersionString());
// Build the URLFetcher to perform the request.
scoped_ptr<net::URLFetcher> url_fetcher =
net::URLFetcher::Create(url, net::URLFetcher::POST, this);
// Binary encode a basic request proto.
image_collections::ClusterRequest request_proto;
request_proto.set_cluster_all(true);
std::string proto_output;
bool result = request_proto.SerializePartialToString(&proto_output);
DCHECK(result);
url_fetcher->SetUploadData("application/octet-stream", proto_output);
return url_fetcher;
}
bool BookmarkServerClusterService::ProcessResponse(const std::string& response,
bool* should_notify) {
DCHECK(*should_notify);
image_collections::ClusterResponse response_proto;
bool result = response_proto.ParseFromString(response);
if (!result)
return false; // Not formatted properly.
ClusterMap new_cluster_data;
for (const auto& cluster : response_proto.clusters()) {
const std::string& title = cluster.title();
if (title.empty())
continue;
std::vector<std::string> stars_ids;
for (auto& doc : cluster.docs()) {
if (!doc.empty())
stars_ids.push_back(doc);
}
if (stars_ids.size())
new_cluster_data[title] = stars_ids;
}
if (new_cluster_data.size() == cluster_data_.size() &&
std::equal(new_cluster_data.begin(),
new_cluster_data.end(),
cluster_data_.begin())) {
*should_notify = false;
} else {
SwapModel(&new_cluster_data);
}
return true;
}
void BookmarkServerClusterService::CleanAfterFailure() {
if (cluster_data_.empty())
return;
ClusterMap empty;
SwapModel(&empty);
}
void BookmarkServerClusterService::EnhancedBookmarkModelLoaded() {
TriggerTokenRequest(false);
}
void BookmarkServerClusterService::EnhancedBookmarkAdded(
const BookmarkNode* node) {
InvalidateCache();
}
void BookmarkServerClusterService::EnhancedBookmarkRemoved(
const BookmarkNode* node) {
// It is possible to remove the entries from the map here, but as those are
// filtered in ClustersForBookmark() this is not strictly necessary.
InvalidateCache();
}
void BookmarkServerClusterService::EnhancedBookmarkNodeChanged(
const BookmarkNode* node) {
InvalidateCache();
}
void BookmarkServerClusterService::EnhancedBookmarkAllUserNodesRemoved() {
if (!cluster_data_.empty()) {
ClusterMap empty;
SwapModel(&empty);
}
}
void BookmarkServerClusterService::EnhancedBookmarkRemoteIdChanged(
const BookmarkNode* node,
const std::string& old_remote_id,
const std::string& remote_id) {
std::vector<std::string> clusters;
for (auto& pair : cluster_data_) {
std::vector<std::string>& stars_ids = pair.second;
std::replace(stars_ids.begin(), stars_ids.end(), old_remote_id, remote_id);
}
}
void BookmarkServerClusterService::GoogleSignedOut(
const std::string& account_id,
const std::string& username) {
if (!cluster_data_.empty()) {
ClusterMap empty;
SwapModel(&empty);
}
}
void BookmarkServerClusterService::SwapModel(ClusterMap* cluster_map) {
cluster_data_.swap(*cluster_map);
const std::string& auth_id = GetSigninManager()->GetAuthenticatedAccountId();
scoped_ptr<base::DictionaryValue> dictionary(
Serialize(cluster_data_, auth_id));
pref_service_->Set(prefs::kBookmarkClusters, *dictionary);
}
void BookmarkServerClusterService::LoadModel() {
const base::DictionaryValue* dictionary =
pref_service_->GetDictionary(prefs::kBookmarkClusters);
const std::string& auth_id = GetSigninManager()->GetAuthenticatedAccountId();
ClusterMap loaded_data;
bool result = BookmarkServerClusterService::Deserialize(
*dictionary, auth_id, &loaded_data);
if (result)
cluster_data_.swap(loaded_data);
}
void BookmarkServerClusterService::OnStateChanged() {
// Do nothing.
}
void BookmarkServerClusterService::OnSyncCycleCompleted() {
// The stars cluster API relies on the information in chrome-sync. Sending a
// cluster request immediately after a bookmark is changed from the bookmark
// observer notification will yield the wrong results. The request must be
// delayed until the sync cycle has completed.
// Note that we will be skipping calling this cluster API if there is no
// observer attached, because calling that is meaningless without UI to show.
// We also will avoid requesting for clusters if the bookmark data hasn't
// changed.
if (refreshes_needed_ > 0) {
DCHECK(model_->loaded());
if (observers_.might_have_observers()) {
TriggerTokenRequest(false);
sync_refresh_skipped_ = false;
} else {
sync_refresh_skipped_ = true;
}
--refreshes_needed_;
}
}
void BookmarkServerClusterService::InvalidateCache() {
// Bookmark changes can happen locally or via sync. It is difficult to
// determine if a given SyncCycle contains all the local modifications.
//
// Consider the following sequence:
// 1. SyncCycleBeginning (bookmark version:1)
// 2. Bookmarks mutate locally (bookmark version:2)
// 3. SyncCycleCompleted (bookmark version:1)
//
// In this case, the bookmarks modified locally won't be sent to the server
// until the next SyncCycleCompleted. Since we can't accurately determine
// if a bookmark change has been sent on a SyncCycleCompleted, we're always
// assuming that we need to wait for 2 sync cycles.
refreshes_needed_ = 2;
}
//
// Serialization.
//
// static
scoped_ptr<base::DictionaryValue> BookmarkServerClusterService::Serialize(
const ClusterMap& cluster_map,
const std::string& auth_id) {
// Create a list of all clusters. For each cluster, make another list. The
// first element in the list is the key (cluster name). All subsequent
// elements are stars ids.
scoped_ptr<base::ListValue> all_clusters(new base::ListValue);
for (auto& pair : cluster_map) {
scoped_ptr<base::ListValue> cluster(new base::ListValue);
cluster->AppendString(pair.first);
cluster->AppendStrings(pair.second);
all_clusters->Append(cluster.release());
}
// The dictionary that will be serialized has two fields: a version field and
// a data field.
scoped_ptr<base::DictionaryValue> data(new base::DictionaryValue);
data->SetInteger(kPrefServiceVersionKey, kPrefServiceVersion);
data->Set(kPrefServiceDataKey, all_clusters.release());
data->SetString(kAuthIdKey, auth_id);
return data.Pass();
}
// static
bool BookmarkServerClusterService::Deserialize(
const base::DictionaryValue& value,
const std::string& auth_id,
ClusterMap* out_map) {
ClusterMap output;
// Check version.
int version;
if (!value.GetInteger(kPrefServiceVersionKey, &version))
return false;
if (version != kPrefServiceVersion)
return false;
// Check auth id.
std::string id;
if (!value.GetString(kAuthIdKey, &id))
return false;
if (id != auth_id)
return false;
const base::ListValue* all_clusters = NULL;
if (!value.GetList(kPrefServiceDataKey, &all_clusters))
return false;
for (size_t index = 0; index < all_clusters->GetSize(); ++index) {
const base::ListValue* cluster = NULL;
if (!all_clusters->GetList(index, &cluster))
return false;
if (cluster->GetSize() < 1)
return false;
std::string key;
if (!cluster->GetString(0, &key))
return false;
std::vector<std::string> stars_ids;
for (size_t index = 1; index < cluster->GetSize(); ++index) {
std::string stars_id;
if (!cluster->GetString(index, &stars_id))
return false;
stars_ids.push_back(stars_id);
}
output.insert(std::make_pair(key, stars_ids));
}
out_map->swap(output);
return true;
}
} // namespace enhanced_bookmarks