chromium/chrome/browser/reading_list/android/reading_list_manager_impl.cc

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/reading_list/android/reading_list_manager_impl.h"

#include <utility>

#include "base/location.h"
#include "base/logging.h"
#include "base/memory/scoped_refptr.h"
#include "base/notreached.h"
#include "base/strings/utf_string_conversions.h"
#include "base/uuid.h"
#include "chrome/grit/generated_resources.h"
#include "components/bookmarks/browser/bookmark_node.h"
#include "components/bookmarks/browser/bookmark_utils.h"
#include "components/reading_list/core/reading_list_model.h"
#include "ui/base/l10n/l10n_util.h"
#include "url/gurl.h"

using BookmarkNode = bookmarks::BookmarkNode;

constexpr char kReadStatusKey[] = "read_status";
constexpr char kReadStatusRead[] = "true";
constexpr char kReadStatusUnread[] = "false";

namespace {

// Sync the bookmark node with |entry|. Returns whether the conversion is
// succeeded.
bool SyncToBookmark(const ReadingListEntry& entry, BookmarkNode* bookmark) {
  DCHECK(bookmark);
  std::u16string title;
  if (!base::UTF8ToUTF16(entry.Title().c_str(), entry.Title().size(), &title)) {
    LOG(ERROR) << "Failed to convert the following title to string16:"
               << entry.Title();
    return false;
  }

  bookmark->set_url(entry.URL());
  bookmark->set_date_added(base::Time::UnixEpoch() +
                           base::Microseconds(entry.CreationTime()));
  bookmark->SetTitle(title);
  bookmark->SetMetaInfo(kReadStatusKey,
                        entry.IsRead() ? kReadStatusRead : kReadStatusUnread);
  return true;
}

}  // namespace

ReadingListManagerImpl::ReadingListManagerImpl(
    ReadingListModel* reading_list_model,
    const IdGenerationFunction& id_gen_func)
    : reading_list_model_(reading_list_model),
      id_gen_func_(id_gen_func),
      loaded_(false),
      performing_batch_update_(false),
      changes_applied_during_batch_(false) {
  DCHECK(reading_list_model_);
  root_ = std::make_unique<BookmarkNode>(
      id_gen_func_.Run(), base::Uuid::GenerateRandomV4(), GURL());
  root_->SetTitle(l10n_util::GetStringUTF16(IDS_READ_LATER_TITLE));
  DCHECK(root_->is_folder());
  reading_list_model_->AddObserver(this);
}

ReadingListManagerImpl::~ReadingListManagerImpl() {
  reading_list_model_->RemoveObserver(this);
}

void ReadingListManagerImpl::ReadingListModelLoaded(
    const ReadingListModel* model) {
  // Constructs the bookmark tree.
  root_->DeleteAll();
  for (const auto& url : model->GetKeys()) {
    AddOrUpdateBookmark(model->GetEntryByURL(url).get());
  }

  loaded_ = true;

  for (Observer& observer : observers_)
    observer.ReadingListLoaded();
}

void ReadingListManagerImpl::ReadingListDidAddEntry(
    const ReadingListModel* model,
    const GURL& url,
    reading_list::EntrySource source) {
  AddOrUpdateBookmark(model->GetEntryByURL(url).get());
}

void ReadingListManagerImpl::ReadingListWillRemoveEntry(
    const ReadingListModel* model,
    const GURL& url) {
  RemoveBookmark(url);
}

void ReadingListManagerImpl::ReadingListDidMoveEntry(
    const ReadingListModel* model,
    const GURL& url) {
  DCHECK(reading_list_model_->loaded());
  scoped_refptr<const ReadingListEntry> moved_entry =
      reading_list_model_->GetEntryByURL(url);
  DCHECK(moved_entry);
  AddOrUpdateBookmark(moved_entry.get());
}

void ReadingListManagerImpl::ReadingListDidUpdateEntry(
    const ReadingListModel* model,
    const GURL& url) {
  DCHECK(reading_list_model_->loaded());
  scoped_refptr<const ReadingListEntry> updated_entry =
      reading_list_model_->GetEntryByURL(url);
  DCHECK(updated_entry);
  AddOrUpdateBookmark(updated_entry.get());
}

void ReadingListManagerImpl::ReadingListDidApplyChanges(
    ReadingListModel* model) {
  // Ignores ReadingListDidApplyChanges() invocations during batch update.
  if (performing_batch_update_) {
    changes_applied_during_batch_ = true;
    return;
  }

  NotifyReadingListChanged();
}

void ReadingListManagerImpl::ReadingListModelBeganBatchUpdates(
    const ReadingListModel* model) {
  DCHECK(!changes_applied_during_batch_);
  performing_batch_update_ = true;
}

void ReadingListManagerImpl::ReadingListModelCompletedBatchUpdates(
    const ReadingListModel* model) {
  // Batch update is done -- notify the observers only once, but only if there
  // were actual changes.
  if (changes_applied_during_batch_) {
    NotifyReadingListChanged();
  }

  performing_batch_update_ = false;
  changes_applied_during_batch_ = false;
}

void ReadingListManagerImpl::AddObserver(Observer* observer) {
  observers_.AddObserver(observer);
}

void ReadingListManagerImpl::RemoveObserver(Observer* observer) {
  observers_.RemoveObserver(observer);
}

const BookmarkNode* ReadingListManagerImpl::Add(const GURL& url,
                                                const std::string& title) {
  DCHECK(reading_list_model_->loaded());
  if (!reading_list_model_->IsUrlSupported(url))
    return nullptr;

  // Add or swap the reading list entry.
  const auto& new_entry = reading_list_model_->AddOrReplaceEntry(
      url, title, reading_list::ADDED_VIA_CURRENT_APP,
      /*estimated_read_time=*/base::TimeDelta());
  const auto* node = FindBookmarkByURL(new_entry.URL());
  return node;
}

const BookmarkNode* ReadingListManagerImpl::Get(const GURL& url) const {
  DCHECK(reading_list_model_->loaded());
  return FindBookmarkByURL(url);
}

const BookmarkNode* ReadingListManagerImpl::GetNodeByID(int64_t id) const {
  if (root_->id() == id)
    return root_.get();

  for (const auto& child : root_->children()) {
    if (child->id() == id)
      return child.get();
  }

  return nullptr;
}

void ReadingListManagerImpl::GetMatchingNodes(
    const bookmarks::QueryFields& query,
    size_t max_count,
    std::vector<const BookmarkNode*>* results) {
  if (results->size() >= max_count)
    return;

  auto query_words = bookmarks::ParseBookmarkQuery(query);
  if (query_words.empty())
    return;

  for (const auto& node : root_->children()) {
    if (bookmarks::DoesBookmarkContainWords(node->GetTitle(), node->url(),
                                            query_words)) {
      results->push_back(node.get());
      if (results->size() == max_count)
        break;
    }
  }
}

bool ReadingListManagerImpl::IsReadingListBookmark(
    const BookmarkNode* node) const {
  if (!node)
    return false;

  // Not recursive since there is only one level of children.
  return (root_.get() == node) || (node->parent() == root_.get());
}

void ReadingListManagerImpl::Delete(const GURL& url) {
  DCHECK(reading_list_model_->loaded());
  reading_list_model_->RemoveEntryByURL(url, FROM_HERE);
}

void ReadingListManagerImpl::DeleteAll() {
  DCHECK(reading_list_model_->loaded());
  reading_list_model_->DeleteAllEntries(FROM_HERE);
}

const BookmarkNode* ReadingListManagerImpl::GetRoot() const {
  DCHECK(reading_list_model_->loaded());
  return root_.get();
}

size_t ReadingListManagerImpl::size() const {
  DCHECK(reading_list_model_->loaded());
  return reading_list_model_->size();
}

size_t ReadingListManagerImpl::unread_size() const {
  DCHECK(reading_list_model_->loaded());
  return reading_list_model_->unread_size();
}

void ReadingListManagerImpl::SetTitle(const GURL& url,
                                      const std::u16string& title) {
  DCHECK(reading_list_model_->loaded());
  scoped_refptr<const ReadingListEntry> entry =
      reading_list_model_->GetEntryByURL(url);
  if (!entry)
    return;

  std::string str_title;
  if (!base::UTF16ToUTF8(title.c_str(), title.size(), &str_title)) {
    LOG(ERROR) << "Failed to convert the following title to string16:" << title;
    return;
  }
  reading_list_model_->SetEntryTitleIfExists(url, str_title);
}

void ReadingListManagerImpl::SetReadStatus(const GURL& url, bool read) {
  DCHECK(reading_list_model_->loaded());
  scoped_refptr<const ReadingListEntry> entry =
      reading_list_model_->GetEntryByURL(url);
  if (!entry)
    return;

  reading_list_model_->SetReadStatusIfExists(url, read);
  auto* node = FindBookmarkByURL(url);
  if (node) {
    node->SetMetaInfo(kReadStatusKey,
                      read ? kReadStatusRead : kReadStatusUnread);
  }
}

bool ReadingListManagerImpl::GetReadStatus(
    const bookmarks::BookmarkNode* node) {
  if (node == root_.get())
    return false;

  std::string value;
  node->GetMetaInfo(kReadStatusKey, &value);

  if (value == kReadStatusRead)
    return true;
  if (value == kReadStatusUnread)
    return false;

  NOTREACHED_IN_MIGRATION() << "May not be reading list node.";
  return false;
}

bool ReadingListManagerImpl::IsLoaded() const {
  return loaded_;
}

BookmarkNode* ReadingListManagerImpl::FindBookmarkByURL(const GURL& url) const {
  if (!url.is_valid())
    return nullptr;

  for (const auto& child : root_->children()) {
    if (url == child->url())
      return child.get();
  }
  return nullptr;
}

// Removes a reading list bookmark node by |url|.
void ReadingListManagerImpl::RemoveBookmark(const GURL& url) {
  const BookmarkNode* node = FindBookmarkByURL(url);
  if (node)
    root_->Remove(root_->GetIndexOf(node).value());
}

const BookmarkNode* ReadingListManagerImpl::AddOrUpdateBookmark(
    const ReadingListEntry* entry) {
  if (!entry)
    return nullptr;

  // Update the existing bookmark node if possible.
  BookmarkNode* node = FindBookmarkByURL(entry->URL());
  if (node) {
    bool success = SyncToBookmark(*entry, node);
    return success ? node : nullptr;
  }

  // Add a new node.
  auto new_node = std::make_unique<BookmarkNode>(
      id_gen_func_.Run(), base::Uuid::GenerateRandomV4(), entry->URL());
  bool success = SyncToBookmark(*entry, new_node.get());
  return success ? root_->Add(std::move(new_node)) : nullptr;
}

void ReadingListManagerImpl::NotifyReadingListChanged() {
  for (Observer& observer : observers_)
    observer.ReadingListChanged();
}