diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2017-07-12 14:07:37 +0200 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2017-07-17 10:29:26 +0000 |
commit | ec02ee4181c49b61fce1c8fb99292dbb8139cc90 (patch) | |
tree | 25cde714b2b71eb639d1cd53f5a22e9ba76e14ef /chromium/components/ntp_snippets/reading_list | |
parent | bb09965444b5bb20b096a291445170876225268d (diff) | |
download | qtwebengine-chromium-ec02ee4181c49b61fce1c8fb99292dbb8139cc90.tar.gz |
BASELINE: Update Chromium to 59.0.3071.134
Change-Id: Id02ef6fb2204c5fd21668a1c3e6911c83b17585a
Reviewed-by: Alexandru Croitor <alexandru.croitor@qt.io>
Diffstat (limited to 'chromium/components/ntp_snippets/reading_list')
5 files changed, 562 insertions, 0 deletions
diff --git a/chromium/components/ntp_snippets/reading_list/reading_list_distillation_state_util.cc b/chromium/components/ntp_snippets/reading_list/reading_list_distillation_state_util.cc new file mode 100644 index 00000000000..e7361ad1300 --- /dev/null +++ b/chromium/components/ntp_snippets/reading_list/reading_list_distillation_state_util.cc @@ -0,0 +1,50 @@ +// Copyright 2017 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/ntp_snippets/reading_list/reading_list_distillation_state_util.h" + +#include "base/logging.h" + +namespace ntp_snippets { + +ReadingListEntry::DistillationState ReadingListStateFromSuggestionState( + ReadingListSuggestionExtra::ReadingListSuggestionDistilledState + distilled_state) { + switch (distilled_state) { + case ReadingListSuggestionExtra::ReadingListSuggestionDistilledState:: + PENDING: + return ReadingListEntry::WAITING; + case ReadingListSuggestionExtra::ReadingListSuggestionDistilledState:: + SUCCESS: + return ReadingListEntry::PROCESSED; + case ReadingListSuggestionExtra::ReadingListSuggestionDistilledState:: + FAILURE: + return ReadingListEntry::DISTILLATION_ERROR; + } + NOTREACHED(); + return ReadingListEntry::PROCESSING; +} + +ReadingListSuggestionExtra::ReadingListSuggestionDistilledState +SuggestionStateFromReadingListState( + ReadingListEntry::DistillationState distilled_state) { + switch (distilled_state) { + case ReadingListEntry::WILL_RETRY: + case ReadingListEntry::PROCESSING: + case ReadingListEntry::WAITING: + return ReadingListSuggestionExtra::ReadingListSuggestionDistilledState:: + PENDING; + case ReadingListEntry::PROCESSED: + return ReadingListSuggestionExtra::ReadingListSuggestionDistilledState:: + SUCCESS; + case ReadingListEntry::DISTILLATION_ERROR: + return ReadingListSuggestionExtra::ReadingListSuggestionDistilledState:: + FAILURE; + } + NOTREACHED(); + return ReadingListSuggestionExtra::ReadingListSuggestionDistilledState:: + PENDING; +} + +} // namespace ntp_snippets diff --git a/chromium/components/ntp_snippets/reading_list/reading_list_distillation_state_util.h b/chromium/components/ntp_snippets/reading_list/reading_list_distillation_state_util.h new file mode 100644 index 00000000000..a00553fc98a --- /dev/null +++ b/chromium/components/ntp_snippets/reading_list/reading_list_distillation_state_util.h @@ -0,0 +1,23 @@ +// Copyright 2017 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. + +#ifndef COMPONENTS_NTP_SNIPPETS_READING_LIST_READING_LIST_DISTILLATION_STATE_UTIL_H_ +#define COMPONENTS_NTP_SNIPPETS_READING_LIST_READING_LIST_DISTILLATION_STATE_UTIL_H_ + +#include "components/ntp_snippets/content_suggestion.h" +#include "components/reading_list/core/reading_list_entry.h" + +namespace ntp_snippets { + +ReadingListEntry::DistillationState ReadingListStateFromSuggestionState( + ReadingListSuggestionExtra::ReadingListSuggestionDistilledState + distilled_state); + +ReadingListSuggestionExtra::ReadingListSuggestionDistilledState +SuggestionStateFromReadingListState( + ReadingListEntry::DistillationState distilled_state); + +} // namespace ntp_snippets + +#endif // COMPONENTS_NTP_SNIPPETS_READING_LIST_READING_LIST_DISTILLATION_STATE_UTIL_H_ diff --git a/chromium/components/ntp_snippets/reading_list/reading_list_suggestions_provider.cc b/chromium/components/ntp_snippets/reading_list/reading_list_suggestions_provider.cc new file mode 100644 index 00000000000..ed11ed83fda --- /dev/null +++ b/chromium/components/ntp_snippets/reading_list/reading_list_suggestions_provider.cc @@ -0,0 +1,248 @@ +// Copyright 2017 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/ntp_snippets/reading_list/reading_list_suggestions_provider.h" + +#include <algorithm> +#include <vector> + +#include "base/bind.h" +#include "base/memory/ptr_util.h" +#include "base/strings/utf_string_conversions.h" +#include "base/threading/thread_task_runner_handle.h" +#include "components/ntp_snippets/category.h" +#include "components/ntp_snippets/reading_list/reading_list_distillation_state_util.h" +#include "components/reading_list/core/reading_list_entry.h" +#include "components/reading_list/core/reading_list_model.h" +#include "components/strings/grit/components_strings.h" +#include "components/url_formatter/url_formatter.h" +#include "ui/base/l10n/l10n_util.h" +#include "ui/gfx/image/image.h" + +namespace ntp_snippets { + +namespace { +// Max number of entries to return. +const int kMaxEntries = 3; + +bool CompareEntries(const ReadingListEntry* lhs, const ReadingListEntry* rhs) { + return lhs->UpdateTime() > rhs->UpdateTime(); +} +} + +ReadingListSuggestionsProvider::ReadingListSuggestionsProvider( + ContentSuggestionsProvider::Observer* observer, + ReadingListModel* reading_list_model) + : ContentSuggestionsProvider(observer), + category_status_(CategoryStatus::AVAILABLE_LOADING), + provided_category_( + Category::FromKnownCategory(KnownCategories::READING_LIST)), + reading_list_model_(reading_list_model), + scoped_observer_(this) { + observer->OnCategoryStatusChanged(this, provided_category_, category_status_); + + // If the ReadingListModel is loaded, this will trigger a call to + // ReadingListModelLoaded. Keep it as last instruction. + scoped_observer_.Add(reading_list_model_); +} + +ReadingListSuggestionsProvider::~ReadingListSuggestionsProvider(){}; + +CategoryStatus ReadingListSuggestionsProvider::GetCategoryStatus( + Category category) { + DCHECK_EQ(category, provided_category_); + return category_status_; +} + +CategoryInfo ReadingListSuggestionsProvider::GetCategoryInfo( + Category category) { + DCHECK_EQ(category, provided_category_); + + return CategoryInfo(l10n_util::GetStringUTF16( + IDS_NTP_READING_LIST_SUGGESTIONS_SECTION_HEADER), + ContentSuggestionsCardLayout::FULL_CARD, + ContentSuggestionsAdditionalAction::VIEW_ALL, + /*show_if_empty=*/false, + l10n_util::GetStringUTF16( + IDS_NTP_READING_LIST_SUGGESTIONS_SECTION_EMPTY)); +} + +void ReadingListSuggestionsProvider::DismissSuggestion( + const ContentSuggestion::ID& suggestion_id) { + if (!reading_list_model_) { + return; + } + + DCHECK(reading_list_model_->loaded()); + GURL url(suggestion_id.id_within_category()); + SetDismissedState(url, true); +} + +void ReadingListSuggestionsProvider::FetchSuggestionImage( + const ContentSuggestion::ID& suggestion_id, + const ImageFetchedCallback& callback) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::Bind(callback, gfx::Image())); +} + +void ReadingListSuggestionsProvider::Fetch( + const Category& category, + const std::set<std::string>& known_suggestion_ids, + const FetchDoneCallback& callback) { + LOG(DFATAL) << "ReadingListSuggestionsProvider has no |Fetch| functionality!"; + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, + Status(StatusCode::PERMANENT_ERROR, + "ReadingListSuggestionsProvider has no |Fetch| " + "functionality!"), + base::Passed(std::vector<ContentSuggestion>()))); +} + +void ReadingListSuggestionsProvider::ClearHistory( + base::Time begin, + base::Time end, + const base::Callback<bool(const GURL& url)>& filter) { + // Ignored, Reading List does not depend on history. +} + +void ReadingListSuggestionsProvider::ClearCachedSuggestions(Category category) { + DCHECK_EQ(category, provided_category_); + // Ignored. +} + +void ReadingListSuggestionsProvider::GetDismissedSuggestionsForDebugging( + Category category, + const DismissedSuggestionsCallback& callback) { + if (!reading_list_model_ || reading_list_model_->IsPerformingBatchUpdates()) { + callback.Run(std::vector<ContentSuggestion>()); + return; + } + + DCHECK(reading_list_model_->loaded()); + std::vector<const ReadingListEntry*> entries; + for (const GURL& url : reading_list_model_->Keys()) { + const ReadingListEntry* entry = reading_list_model_->GetEntryByURL(url); + if (entry->ContentSuggestionsExtra()->dismissed) { + entries.emplace_back(entry); + } + } + + std::sort(entries.begin(), entries.end(), CompareEntries); + + std::vector<ContentSuggestion> suggestions; + for (const ReadingListEntry* entry : entries) { + suggestions.emplace_back(ConvertEntry(entry)); + } + + callback.Run(std::move(suggestions)); +} + +void ReadingListSuggestionsProvider::ClearDismissedSuggestionsForDebugging( + Category category) { + for (const auto& url : reading_list_model_->Keys()) { + SetDismissedState(url, false); + } +} + +void ReadingListSuggestionsProvider::ReadingListModelLoaded( + const ReadingListModel* model) { + DCHECK(model == reading_list_model_); + FetchReadingListInternal(); +} + +void ReadingListSuggestionsProvider::ReadingListModelBeingDeleted( + const ReadingListModel* model) { + DCHECK(model == reading_list_model_); + scoped_observer_.Remove(reading_list_model_); + reading_list_model_ = nullptr; +} + +void ReadingListSuggestionsProvider::ReadingListDidApplyChanges( + ReadingListModel* model) { + DCHECK(model == reading_list_model_); + + FetchReadingListInternal(); +} + +void ReadingListSuggestionsProvider::ReadingListModelCompletedBatchUpdates( + const ReadingListModel* model) { + DCHECK(model == reading_list_model_); + + FetchReadingListInternal(); +} + +void ReadingListSuggestionsProvider::FetchReadingListInternal() { + if (!reading_list_model_ || reading_list_model_->IsPerformingBatchUpdates()) { + return; + } + + DCHECK(reading_list_model_->loaded()); + std::vector<const ReadingListEntry*> entries; + for (const GURL& url : reading_list_model_->Keys()) { + const ReadingListEntry* entry = reading_list_model_->GetEntryByURL(url); + if (!entry->IsRead() && !entry->ContentSuggestionsExtra()->dismissed) { + entries.emplace_back(entry); + } + } + + if (entries.size() > kMaxEntries) { + // Get the |kMaxEntries| most recent entries. + std::partial_sort(entries.begin(), entries.begin() + kMaxEntries, + entries.end(), CompareEntries); + entries.resize(kMaxEntries); + } else { + std::sort(entries.begin(), entries.end(), CompareEntries); + } + + std::vector<ContentSuggestion> suggestions; + for (const ReadingListEntry* entry : entries) { + suggestions.emplace_back(ConvertEntry(entry)); + } + + NotifyStatusChanged(CategoryStatus::AVAILABLE); + observer()->OnNewSuggestions(this, provided_category_, + std::move(suggestions)); +} + +ContentSuggestion ReadingListSuggestionsProvider::ConvertEntry( + const ReadingListEntry* entry) { + ContentSuggestion suggestion(provided_category_, entry->URL().spec(), + entry->URL()); + + if (!entry->Title().empty()) { + suggestion.set_title(base::UTF8ToUTF16(entry->Title())); + } else { + suggestion.set_title(url_formatter::FormatUrl(entry->URL())); + } + suggestion.set_publisher_name( + url_formatter::FormatUrl(entry->URL().GetOrigin())); + + auto extra = base::MakeUnique<ReadingListSuggestionExtra>(); + extra->distilled_state = + SuggestionStateFromReadingListState(entry->DistilledState()); + extra->favicon_page_url = + entry->DistilledURL().is_valid() ? entry->DistilledURL() : entry->URL(); + suggestion.set_reading_list_suggestion_extra(std::move(extra)); + + return suggestion; +} + +void ReadingListSuggestionsProvider::NotifyStatusChanged( + CategoryStatus new_status) { + if (category_status_ == new_status) { + return; + } + category_status_ = new_status; + observer()->OnCategoryStatusChanged(this, provided_category_, new_status); +} + +void ReadingListSuggestionsProvider::SetDismissedState(const GURL& url, + bool dismissed) { + reading_list::ContentSuggestionsExtra extra; + extra.dismissed = dismissed; + reading_list_model_->SetContentSuggestionsExtra(url, extra); +} + +} // namespace ntp_snippets diff --git a/chromium/components/ntp_snippets/reading_list/reading_list_suggestions_provider.h b/chromium/components/ntp_snippets/reading_list/reading_list_suggestions_provider.h new file mode 100644 index 00000000000..433d9493b80 --- /dev/null +++ b/chromium/components/ntp_snippets/reading_list/reading_list_suggestions_provider.h @@ -0,0 +1,83 @@ +// Copyright 2017 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. + +#ifndef COMPONENTS_NTP_SNIPPETS_READING_LIST_READING_LIST_SUGGESTIONS_PROVIDER_H_ +#define COMPONENTS_NTP_SNIPPETS_READING_LIST_READING_LIST_SUGGESTIONS_PROVIDER_H_ + +#include <set> +#include <string> + +#include "base/scoped_observer.h" +#include "components/ntp_snippets/callbacks.h" +#include "components/ntp_snippets/category.h" +#include "components/ntp_snippets/category_info.h" +#include "components/ntp_snippets/category_status.h" +#include "components/ntp_snippets/content_suggestion.h" +#include "components/ntp_snippets/content_suggestions_provider.h" +#include "components/reading_list/core/reading_list_model_observer.h" + +class ReadingListModel; + +namespace ntp_snippets { + +// Provides content suggestions from the Reading List. +class ReadingListSuggestionsProvider : public ContentSuggestionsProvider, + public ReadingListModelObserver { + public: + ReadingListSuggestionsProvider(ContentSuggestionsProvider::Observer* observer, + ReadingListModel* reading_list_model); + ~ReadingListSuggestionsProvider() override; + + // ContentSuggestionsProvider implementation. + CategoryStatus GetCategoryStatus(Category category) override; + CategoryInfo GetCategoryInfo(Category category) override; + void DismissSuggestion(const ContentSuggestion::ID& suggestion_id) override; + void FetchSuggestionImage(const ContentSuggestion::ID& suggestion_id, + const ImageFetchedCallback& callback) override; + void Fetch(const Category& category, + const std::set<std::string>& known_suggestion_ids, + const FetchDoneCallback& callback) override; + void ClearHistory( + base::Time begin, + base::Time end, + const base::Callback<bool(const GURL& url)>& filter) override; + void ClearCachedSuggestions(Category category) override; + void GetDismissedSuggestionsForDebugging( + Category category, + const DismissedSuggestionsCallback& callback) override; + void ClearDismissedSuggestionsForDebugging(Category category) override; + + // ReadingListModelObserver implementation. + void ReadingListModelLoaded(const ReadingListModel* model) override; + void ReadingListModelBeingDeleted(const ReadingListModel* model) override; + void ReadingListDidApplyChanges(ReadingListModel* model) override; + void ReadingListModelCompletedBatchUpdates( + const ReadingListModel* model) override; + + private: + // The actual method to fetch Reading List entries. Must be called after the + // model is loaded. + void FetchReadingListInternal(); + + // Converts |entry| to ContentSuggestion. + ContentSuggestion ConvertEntry(const ReadingListEntry* entry); + + // Updates the |category_status_| and notifies the |observer_|, if necessary. + void NotifyStatusChanged(CategoryStatus new_status); + + // Sets the dismissed status of the entry to |dismissed|. + void SetDismissedState(const GURL& url, bool dismissed); + + CategoryStatus category_status_; + const Category provided_category_; + + ReadingListModel* reading_list_model_; + ScopedObserver<ReadingListModel, ReadingListModelObserver> scoped_observer_; + + DISALLOW_COPY_AND_ASSIGN(ReadingListSuggestionsProvider); +}; + +} // namespace ntp_snippets + +#endif // COMPONENTS_NTP_SNIPPETS_READING_LIST_READING_LIST_SUGGESTIONS_PROVIDER_H_ diff --git a/chromium/components/ntp_snippets/reading_list/reading_list_suggestions_provider_unittest.cc b/chromium/components/ntp_snippets/reading_list/reading_list_suggestions_provider_unittest.cc new file mode 100644 index 00000000000..3ed07071891 --- /dev/null +++ b/chromium/components/ntp_snippets/reading_list/reading_list_suggestions_provider_unittest.cc @@ -0,0 +1,158 @@ +// Copyright 2017 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/ntp_snippets/reading_list/reading_list_suggestions_provider.h" + +#include "base/memory/ptr_util.h" +#include "base/test/simple_test_clock.h" +#include "components/ntp_snippets/mock_content_suggestions_provider_observer.h" +#include "components/reading_list/core/reading_list_model_impl.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace ntp_snippets { + +namespace { + +const char kTitleUnread1[] = "title1"; +const char kTitleUnread2[] = "title2"; +const char kTitleUnread3[] = "title3"; +const char kTitleUnread4[] = "title4"; +const char kTitleRead1[] = "title_read1"; + +using ::testing::_; +using ::testing::ElementsAre; +using ::testing::IsEmpty; +using ::testing::Property; + +class ReadingListSuggestionsProviderTest : public ::testing::Test { + public: + ReadingListSuggestionsProviderTest() { + std::unique_ptr<base::SimpleTestClock> clock = + base::MakeUnique<base::SimpleTestClock>(); + clock_ = clock.get(); + model_ = base::MakeUnique<ReadingListModelImpl>( + /*storage_layer=*/nullptr, /*pref_service=*/nullptr, std::move(clock)); + } + + void CreateProvider() { + EXPECT_CALL(observer_, + OnCategoryStatusChanged(_, ReadingListCategory(), + CategoryStatus::AVAILABLE_LOADING)) + .RetiresOnSaturation(); + EXPECT_CALL(observer_, OnCategoryStatusChanged(_, ReadingListCategory(), + CategoryStatus::AVAILABLE)) + .RetiresOnSaturation(); + provider_ = base::MakeUnique<ReadingListSuggestionsProvider>(&observer_, + model_.get()); + } + + Category ReadingListCategory() { + return Category::FromKnownCategory(KnownCategories::READING_LIST); + } + + void AddEntries() { + model_->AddEntry(url_unread1_, kTitleUnread1, + reading_list::ADDED_VIA_CURRENT_APP); + clock_->Advance(base::TimeDelta::FromMilliseconds(10)); + model_->AddEntry(url_unread2_, kTitleUnread2, + reading_list::ADDED_VIA_CURRENT_APP); + clock_->Advance(base::TimeDelta::FromMilliseconds(10)); + model_->AddEntry(url_read1_, kTitleRead1, + reading_list::ADDED_VIA_CURRENT_APP); + model_->SetReadStatus(url_read1_, true); + clock_->Advance(base::TimeDelta::FromMilliseconds(10)); + model_->AddEntry(url_unread3_, kTitleUnread3, + reading_list::ADDED_VIA_CURRENT_APP); + clock_->Advance(base::TimeDelta::FromMilliseconds(10)); + model_->AddEntry(url_unread4_, kTitleUnread4, + reading_list::ADDED_VIA_CURRENT_APP); + } + + protected: + base::SimpleTestClock* clock_; + std::unique_ptr<ReadingListModelImpl> model_; + testing::StrictMock<MockContentSuggestionsProviderObserver> observer_; + std::unique_ptr<ReadingListSuggestionsProvider> provider_; + + const GURL url_unread1_{"http://www.foo1.bar"}; + const GURL url_unread2_{"http://www.foo2.bar"}; + const GURL url_unread3_{"http://www.foo3.bar"}; + const GURL url_unread4_{"http://www.foo4.bar"}; + const GURL url_read1_{"http://www.bar.foor"}; +}; + +TEST_F(ReadingListSuggestionsProviderTest, CategoryInfo) { + EXPECT_CALL(observer_, OnNewSuggestions(_, ReadingListCategory(), IsEmpty())) + .RetiresOnSaturation(); + CreateProvider(); + + CategoryInfo categoryInfo = provider_->GetCategoryInfo(ReadingListCategory()); + EXPECT_EQ(ContentSuggestionsAdditionalAction::VIEW_ALL, + categoryInfo.additional_action()); +} + +TEST_F(ReadingListSuggestionsProviderTest, ReturnsThreeLatestUnreadSuggestion) { + AddEntries(); + + EXPECT_CALL( + observer_, + OnNewSuggestions( + _, ReadingListCategory(), + ElementsAre(Property(&ContentSuggestion::url, url_unread4_), + Property(&ContentSuggestion::url, url_unread3_), + Property(&ContentSuggestion::url, url_unread2_)))); + + CreateProvider(); +} + +// Tests that the provider returns only unread suggestions even if there is less +// unread suggestions than the maximum number of suggestions. +TEST_F(ReadingListSuggestionsProviderTest, ReturnsOnlyUnreadSuggestion) { + GURL url_unread1 = GURL("http://www.foo1.bar"); + GURL url_read1 = GURL("http://www.bar.foor"); + std::string title_unread1 = "title1"; + std::string title_read1 = "title_read1"; + model_->AddEntry(url_unread1, title_unread1, + reading_list::ADDED_VIA_CURRENT_APP); + clock_->Advance(base::TimeDelta::FromMilliseconds(10)); + model_->AddEntry(url_read1, title_read1, reading_list::ADDED_VIA_CURRENT_APP); + model_->SetReadStatus(url_read1, true); + + EXPECT_CALL(observer_, + OnNewSuggestions( + _, ReadingListCategory(), + ElementsAre(Property(&ContentSuggestion::url, url_unread1)))); + + CreateProvider(); +} + +TEST_F(ReadingListSuggestionsProviderTest, DismissesEntry) { + AddEntries(); + + EXPECT_CALL( + observer_, + OnNewSuggestions( + _, ReadingListCategory(), + ElementsAre(Property(&ContentSuggestion::url, url_unread4_), + Property(&ContentSuggestion::url, url_unread3_), + Property(&ContentSuggestion::url, url_unread2_)))); + + CreateProvider(); + + EXPECT_CALL( + observer_, + OnNewSuggestions( + _, ReadingListCategory(), + ElementsAre(Property(&ContentSuggestion::url, url_unread4_), + Property(&ContentSuggestion::url, url_unread2_), + Property(&ContentSuggestion::url, url_unread1_)))); + + provider_->DismissSuggestion( + ContentSuggestion::ID(ReadingListCategory(), url_unread3_.spec())); +} + +} // namespace + +} // namespace ntp_snippets |