// 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 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 BookmarkServerClusterService::BookmarksForClusterNamed( const std::string& cluster_name) const { std::vector 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 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 clusters; for (auto& pair : cluster_data_) { const std::vector& 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 BookmarkServerClusterService::GetClusters() const { std::vector 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 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 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 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 clusters; for (auto& pair : cluster_data_) { std::vector& 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 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 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 all_clusters(new base::ListValue); for (auto& pair : cluster_map) { scoped_ptr 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 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 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