Commit 29349eee authored by Dan Harrington's avatar Dan Harrington Committed by Commit Bot

Provide thumbnails in AvailableOfflineContentProvider

Shows thumbnails on the net error page, but UI in this CL is a placeholder.

Bug: 852872
Change-Id: I4710058e835318fca3f84eb52752c261ba2fcc36
Reviewed-on: https://chromium-review.googlesource.com/1157265Reviewed-by: default avatarMin Qin <qinmin@chromium.org>
Reviewed-by: default avatarCarlos Knippschild <carlosk@chromium.org>
Reviewed-by: default avatarEdward Jung <edwardjung@chromium.org>
Commit-Queue: Dan H <harringtond@google.com>
Cr-Commit-Position: refs/heads/master@{#584509}
parent 3033eaec
......@@ -4,6 +4,8 @@
#include "chrome/browser/android/download/available_offline_content_provider.h"
#include "base/base64.h"
#include "base/strings/strcat.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "chrome/browser/android/chrome_feature_list.h"
......@@ -65,9 +67,110 @@ bool CompareItemsByUsefulness(const OfflineItem& a, const OfflineItem& b) {
return a.id < b.id;
}
class ThumbnailFetch {
public:
// Gets visuals for a list of thumbnails. Calls |complete_callback| with
// a list of data URIs containing the thumbnails for |content_ids|, in the
// same order. If no thumbnail is available, the corresponding result
// string is left empty.
static void Start(
offline_items_collection::OfflineContentAggregator* aggregator,
std::vector<offline_items_collection::ContentId> content_ids,
base::OnceCallback<void(std::vector<GURL>)> complete_callback) {
// ThumbnailFetch instances are self-deleting.
ThumbnailFetch* fetch = new ThumbnailFetch(std::move(content_ids),
std::move(complete_callback));
fetch->Start(aggregator);
}
private:
ThumbnailFetch(std::vector<offline_items_collection::ContentId> content_ids,
base::OnceCallback<void(std::vector<GURL>)> complete_callback)
: content_ids_(std::move(content_ids)),
complete_callback_(std::move(complete_callback)) {
thumbnails_.resize(content_ids_.size());
}
void Start(offline_items_collection::OfflineContentAggregator* aggregator) {
if (content_ids_.empty()) {
Complete();
return;
}
auto callback = base::BindRepeating(&ThumbnailFetch::VisualsReceived,
base::Unretained(this));
for (offline_items_collection::ContentId id : content_ids_) {
aggregator->GetVisualsForItem(id, callback);
}
}
void VisualsReceived(
const offline_items_collection::ContentId& id,
std::unique_ptr<offline_items_collection::OfflineItemVisuals> visuals) {
DCHECK(callback_count_ < content_ids_.size());
AddVisual(id, std::move(visuals));
if (++callback_count_ == content_ids_.size())
Complete();
}
void Complete() {
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::BindOnce(std::move(complete_callback_), std::move(thumbnails_)));
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::BindOnce(
[](ThumbnailFetch* thumbnail_fetch) { delete thumbnail_fetch; },
this));
}
void AddVisual(
const offline_items_collection::ContentId& id,
std::unique_ptr<offline_items_collection::OfflineItemVisuals> visuals) {
if (!visuals)
return;
scoped_refptr<base::RefCountedMemory> data = visuals->icon.As1xPNGBytes();
if (!data || data->size() == 0)
return;
std::string content_base64;
base::Base64Encode(base::StringPiece(data->front_as<char>(), data->size()),
&content_base64);
for (size_t i = 0; i < content_ids_.size(); ++i) {
if (content_ids_[i] == id) {
thumbnails_[i] =
GURL(base::StrCat({"data:image/png;base64,", content_base64}));
break;
}
}
}
// The list of item IDs for which to fetch visuals.
std::vector<offline_items_collection::ContentId> content_ids_;
// The thumbnail data URIs to be returned. |thumbnails_| size is equal to
// |content_ids_| size.
std::vector<GURL> thumbnails_;
base::OnceCallback<void(std::vector<GURL>)> complete_callback_;
size_t callback_count_ = 0;
DISALLOW_COPY_AND_ASSIGN(ThumbnailFetch);
};
chrome::mojom::AvailableOfflineContentPtr CreateAvailableOfflineContent(
const OfflineItem& item,
const GURL& thumbnail_url) {
return chrome::mojom::AvailableOfflineContent::New(
item.id.id, item.id.name_space, item.title, item.description,
base::UTF16ToASCII(ui::TimeFormat::Simple(
ui::TimeFormat::FORMAT_ELAPSED, ui::TimeFormat::LENGTH_SHORT,
base::Time::Now() - item.creation_time)),
"", // TODO(crbug.com/852872): Add attribution
std::move(thumbnail_url));
}
// Picks the best available offline content items, and passes them to callback.
void ListFinalize(AvailableOfflineContentProvider::ListCallback callback,
const std::vector<OfflineItem>& all_items) {
void ListFinalize(
AvailableOfflineContentProvider::ListCallback callback,
offline_items_collection::OfflineContentAggregator* aggregator,
const std::vector<OfflineItem>& all_items) {
// Save the best 3 or fewer times to |selected|.
const int kMaxItemsToReturn = 3;
std::vector<OfflineItem> selected(kMaxItemsToReturn);
......@@ -76,22 +179,29 @@ void ListFinalize(AvailableOfflineContentProvider::ListCallback callback,
CompareItemsByUsefulness);
selected.resize(end - selected.begin());
// Translate OfflineItem to AvailableOfflineContentPtr, and filter out
// items that should not be shown.
std::vector<chrome::mojom::AvailableOfflineContentPtr> result;
for (const OfflineItem& item : selected) {
if (ContentTypePriority(item) >= ContentPriority::kDoNotShow)
break;
result.push_back(chrome::mojom::AvailableOfflineContent::New(
item.id.id, item.id.name_space, item.title, item.description,
base::UTF16ToASCII(ui::TimeFormat::Simple(
ui::TimeFormat::FORMAT_ELAPSED, ui::TimeFormat::LENGTH_SHORT,
base::Time::Now() - item.creation_time)),
"", // TODO(crbug.com/852872): Add attribution
GURL() // TODO(crbug.com/852872): Add thumbnail data URL
));
}
std::move(callback).Run(std::move(result));
while (!selected.empty() &&
ContentTypePriority(selected.back()) >= ContentPriority::kDoNotShow)
selected.pop_back();
std::vector<offline_items_collection::ContentId> selected_ids;
for (const OfflineItem& item : selected)
selected_ids.push_back(item.id);
auto complete = [](AvailableOfflineContentProvider::ListCallback callback,
std::vector<OfflineItem> selected,
std::vector<GURL> thumbnail_data_uris) {
// Translate OfflineItem to AvailableOfflineContentPtr.
std::vector<chrome::mojom::AvailableOfflineContentPtr> result;
for (size_t i = 0; i < selected.size(); ++i) {
result.push_back(
CreateAvailableOfflineContent(selected[i], thumbnail_data_uris[i]));
}
std::move(callback).Run(std::move(result));
};
ThumbnailFetch::Start(
aggregator, selected_ids,
base::BindOnce(complete, std::move(callback), std::move(selected)));
}
} // namespace
......@@ -109,7 +219,8 @@ void AvailableOfflineContentProvider::List(ListCallback callback) {
}
offline_items_collection::OfflineContentAggregator* aggregator =
OfflineContentAggregatorFactory::GetForBrowserContext(browser_context_);
aggregator->GetAllItems(base::BindOnce(ListFinalize, std::move(callback)));
aggregator->GetAllItems(base::BindOnce(ListFinalize, std::move(callback),
base::Unretained(aggregator)));
}
void AvailableOfflineContentProvider::Create(
......
......@@ -4,15 +4,18 @@
#include "chrome/browser/android/download/available_offline_content_provider.h"
#include "base/strings/string_util.h"
#include "base/test/bind_test_util.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/browser/android/chrome_feature_list.h"
#include "chrome/browser/offline_items_collection/offline_content_aggregator_factory.h"
#include "chrome/test/base/testing_profile.h"
#include "components/offline_items_collection/core/offline_content_aggregator.h"
#include "components/offline_items_collection/core/offline_item.h"
#include "components/offline_items_collection/core/test_support/mock_offline_content_provider.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/image/image_unittest_util.h"
#include "url/gurl.h"
namespace android {
......@@ -20,6 +23,7 @@ namespace {
using offline_items_collection::OfflineContentAggregator;
using testing::_;
const char kProviderNamespace[] = "offline_pages";
std::unique_ptr<KeyedService> BuildOfflineContentAggregator(
content::BrowserContext* context) {
......@@ -28,9 +32,10 @@ std::unique_ptr<KeyedService> BuildOfflineContentAggregator(
offline_items_collection::OfflineItem UselessItem() {
offline_items_collection::OfflineItem item;
item.original_url = GURL("https://uesless");
item.original_url = GURL("https://useless");
item.filter = offline_items_collection::FILTER_IMAGE;
item.id.id = "Useless";
item.id.name_space = kProviderNamespace;
return item;
}
......@@ -39,6 +44,7 @@ offline_items_collection::OfflineItem OldOfflinePage() {
item.original_url = GURL("https://already_read");
item.filter = offline_items_collection::FILTER_PAGE;
item.id.id = "AlreadyRead";
item.id.name_space = kProviderNamespace;
item.is_suggested = true;
item.last_accessed_time = base::Time::Now();
return item;
......@@ -49,7 +55,7 @@ offline_items_collection::OfflineItem SuggestedOfflinePageItem() {
item.original_url = GURL("https://page");
item.filter = offline_items_collection::FILTER_PAGE;
item.id.id = "SuggestedPage";
item.id.name_space = "testnamespace";
item.id.name_space = kProviderNamespace;
item.is_suggested = true;
item.title = "Page Title";
item.description = "snippet";
......@@ -65,6 +71,7 @@ offline_items_collection::OfflineItem VideoItem() {
item.original_url = GURL("https://video");
item.filter = offline_items_collection::FILTER_VIDEO;
item.id.id = "VideoItem";
item.id.name_space = kProviderNamespace;
return item;
}
......@@ -73,39 +80,47 @@ offline_items_collection::OfflineItem AudioItem() {
item.original_url = GURL("https://audio");
item.filter = offline_items_collection::FILTER_AUDIO;
item.id.id = "AudioItem";
item.id.name_space = kProviderNamespace;
return item;
}
offline_items_collection::OfflineItemVisuals TestThumbnail() {
offline_items_collection::OfflineItemVisuals visuals;
visuals.icon = gfx::test::CreateImage(2, 4);
return visuals;
}
class AvailableOfflineContentTest : public testing::Test {
protected:
void SetUp() override {
// To control the items in the aggregator, we create it and register a
// single MockOfflineContentProvider.
aggregator = static_cast<OfflineContentAggregator*>(
aggregator_ = static_cast<OfflineContentAggregator*>(
OfflineContentAggregatorFactory::GetInstance()->SetTestingFactoryAndUse(
&profile, &BuildOfflineContentAggregator));
aggregator->RegisterProvider("offline_pages", &content_provider);
&profile_, &BuildOfflineContentAggregator));
aggregator_->RegisterProvider(kProviderNamespace, &content_provider_);
content_provider_.SetVisuals({});
}
std::vector<chrome::mojom::AvailableOfflineContentPtr> ListAndWait() {
std::vector<chrome::mojom::AvailableOfflineContentPtr> suggestions;
bool received = false;
provider.List(base::BindLambdaForTesting(
provider_.List(base::BindLambdaForTesting(
[&](std::vector<chrome::mojom::AvailableOfflineContentPtr> result) {
received = true;
suggestions = std::move(result);
}));
thread_bundle.RunUntilIdle();
thread_bundle_.RunUntilIdle();
EXPECT_TRUE(received);
return suggestions;
}
content::TestBrowserThreadBundle thread_bundle;
TestingProfile profile;
base::test::ScopedFeatureList scoped_feature_list;
OfflineContentAggregator* aggregator;
offline_items_collection::MockOfflineContentProvider content_provider;
AvailableOfflineContentProvider provider{&profile};
content::TestBrowserThreadBundle thread_bundle_;
TestingProfile profile_;
base::test::ScopedFeatureList scoped_feature_list_;
OfflineContentAggregator* aggregator_;
offline_items_collection::MockOfflineContentProvider content_provider_;
AvailableOfflineContentProvider provider_{&profile_};
};
TEST_F(AvailableOfflineContentTest, NoContent) {
......@@ -116,8 +131,9 @@ TEST_F(AvailableOfflineContentTest, NoContent) {
}
TEST_F(AvailableOfflineContentTest, AllContentFilteredOut) {
scoped_feature_list.InitAndEnableFeature(chrome::android::kNewNetErrorPageUI);
content_provider.SetItems({UselessItem(), OldOfflinePage()});
scoped_feature_list_.InitAndEnableFeature(
chrome::android::kNewNetErrorPageUI);
content_provider_.SetItems({UselessItem(), OldOfflinePage()});
std::vector<chrome::mojom::AvailableOfflineContentPtr> suggestions =
ListAndWait();
......@@ -126,11 +142,15 @@ TEST_F(AvailableOfflineContentTest, AllContentFilteredOut) {
}
TEST_F(AvailableOfflineContentTest, ThreeItems) {
scoped_feature_list.InitAndEnableFeature(chrome::android::kNewNetErrorPageUI);
content_provider.SetItems({
scoped_feature_list_.InitAndEnableFeature(
chrome::android::kNewNetErrorPageUI);
content_provider_.SetItems({
UselessItem(), VideoItem(), SuggestedOfflinePageItem(), AudioItem(),
});
content_provider_.SetVisuals(
{{SuggestedOfflinePageItem().id, TestThumbnail()}});
std::vector<chrome::mojom::AvailableOfflineContentPtr> suggestions =
ListAndWait();
......@@ -150,15 +170,22 @@ TEST_F(AvailableOfflineContentTest, ThreeItems) {
EXPECT_EQ(page_item.title, first->title);
EXPECT_EQ(page_item.description, first->snippet);
EXPECT_EQ("4 hours ago", first->date_modified);
// attribution and thumbnail_data_uri not yet implemented.
// At the time of writing this test, the output was:
// data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAAECAYAAACk7+45AAAAFk
// lEQVQYlWNk+M/wn4GBgYGJAQowGQBCcgIG00vTRwAAAABJRU5ErkJggg==
// Since other encodings are possible, just check the prefix. PNGs all have
// the same 8 byte header.
EXPECT_TRUE(base::StartsWith(first->thumbnail_data_uri.spec(),
"data:image/png;base64,iVBORw0K",
base::CompareCase::SENSITIVE));
// TODO(crbug.com/852872): Add attribution.
EXPECT_EQ("", first->attribution);
EXPECT_EQ("", first->thumbnail_data_uri);
}
TEST_F(AvailableOfflineContentTest, NotEnabled) {
scoped_feature_list.InitAndDisableFeature(
scoped_feature_list_.InitAndDisableFeature(
chrome::android::kNewNetErrorPageUI);
content_provider.SetItems({SuggestedOfflinePageItem()});
content_provider_.SetItems({SuggestedOfflinePageItem()});
std::vector<chrome::mojom::AvailableOfflineContentPtr> suggestions =
ListAndWait();
......
......@@ -367,6 +367,14 @@ html[subframe] body {
color: rgb(255, 255, 255);
}
/*
TODO(https://crbug.com/852872): UI for offline suggested content is incomplete.
*/
.suggested-thumbnail {
width: 25vw;
height: 25vw;
}
/* Alternate dino page button styles */
#control-buttons .reload-button-alternate:disabled {
background: #ccc;
......
......@@ -160,8 +160,16 @@ function offlineContentAvailable(content) {
var div = document.getElementById('offline-suggestions');
var suggestionText = [];
for (var c of content) {
var visual = '';
if (c.thumbnail_data_uri) {
// html_inline.py will try to replace src attributes with data URIs using
// a simple regex. The following is obfuscated slightly to avoid that.
var src = 'src';
visual = `<img ${src}="${c.thumbnail_data_uri}"` +
' class="suggested-thumbnail"></img>';
}
suggestionText.push(
`<li>${c.title} ${c.date_modified} ${c.attribution}</li>`);
`<li>${visual} ${c.title} ${c.date_modified} ${c.attribution}</li>`);
}
var htmlList = document.getElementById('offline-content-list');
htmlList.innerHTML = suggestionText.join('\n');
......
......@@ -185,8 +185,8 @@ TEST_F(OfflineContentAggregatorTest, ActionPropagatesToRightProvider) {
EXPECT_CALL(provider2, ResumeDownload(id2, true)).Times(1);
EXPECT_CALL(provider1, PauseDownload(id1)).Times(1);
EXPECT_CALL(provider2, PauseDownload(id2)).Times(1);
EXPECT_CALL(provider1, GetVisualsForItem(id1, _)).Times(1);
EXPECT_CALL(provider2, GetVisualsForItem(id2, _)).Times(1);
EXPECT_CALL(provider1, GetVisualsForItem_(id1, _)).Times(1);
EXPECT_CALL(provider2, GetVisualsForItem_(id2, _)).Times(1);
EXPECT_CALL(provider1, GetShareInfoForItem(id1, _)).Times(1);
EXPECT_CALL(provider2, GetShareInfoForItem(id2, _)).Times(1);
aggregator_.OpenItem(LaunchLocation::DOWNLOAD_HOME, id1);
......
......@@ -23,6 +23,12 @@ void MockOfflineContentProvider::SetItems(const OfflineItemList& items) {
items_ = items;
}
void MockOfflineContentProvider::SetVisuals(
std::map<ContentId, OfflineItemVisuals> visuals) {
override_visuals_ = true;
visuals_ = std::move(visuals);
}
void MockOfflineContentProvider::NotifyOnItemsAdded(
const OfflineItemList& items) {
for (auto& observer : observers_)
......@@ -39,6 +45,21 @@ void MockOfflineContentProvider::NotifyOnItemUpdated(const OfflineItem& item) {
observer.OnItemUpdated(item);
}
void MockOfflineContentProvider::GetVisualsForItem(const ContentId& id,
VisualsCallback callback) {
if (!override_visuals_) {
GetVisualsForItem_(id, std::move(callback));
} else {
std::unique_ptr<OfflineItemVisuals> visuals;
auto iter = visuals_.find(id);
if (iter != visuals_.end()) {
visuals = std::make_unique<OfflineItemVisuals>(iter->second);
}
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), id, std::move(visuals)));
}
}
void MockOfflineContentProvider::GetAllItems(MultipleItemCallback callback) {
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), items_));
......
......@@ -31,6 +31,9 @@ class MockOfflineContentProvider : public OfflineContentProvider {
bool HasObserver(Observer* observer);
void SetItems(const OfflineItemList& items);
// Sets visuals returned by |GetVisualsForItem()|. If this is not called,
// then the mocked method |GetVisualsForItem_()| is called instead.
void SetVisuals(std::map<ContentId, OfflineItemVisuals> visuals);
void NotifyOnItemsAdded(const OfflineItemList& items);
void NotifyOnItemRemoved(const ContentId& id);
void NotifyOnItemUpdated(const OfflineItem& item);
......@@ -41,7 +44,10 @@ class MockOfflineContentProvider : public OfflineContentProvider {
MOCK_METHOD1(CancelDownload, void(const ContentId&));
MOCK_METHOD1(PauseDownload, void(const ContentId&));
MOCK_METHOD2(ResumeDownload, void(const ContentId&, bool));
MOCK_METHOD2(GetVisualsForItem, void(const ContentId&, VisualsCallback));
MOCK_METHOD2(GetVisualsForItem_,
void(const ContentId&, const VisualsCallback&));
void GetVisualsForItem(const ContentId& id,
VisualsCallback callback) override;
MOCK_METHOD2(GetShareInfoForItem, void(const ContentId&, ShareCallback));
void GetAllItems(MultipleItemCallback callback) override;
void GetItemById(const ContentId& id, SingleItemCallback callback) override;
......@@ -51,6 +57,8 @@ class MockOfflineContentProvider : public OfflineContentProvider {
private:
base::ObserverList<Observer>::Unchecked observers_;
OfflineItemList items_;
std::map<ContentId, OfflineItemVisuals> visuals_;
bool override_visuals_ = false;
};
} // namespace offline_items_collection
......
......@@ -97,7 +97,7 @@ TEST_F(ThrottledOfflineContentProviderTest, TestBasicPassthrough) {
EXPECT_CALL(wrapped_provider_, CancelDownload(id));
EXPECT_CALL(wrapped_provider_, PauseDownload(id));
EXPECT_CALL(wrapped_provider_, ResumeDownload(id, true));
EXPECT_CALL(wrapped_provider_, GetVisualsForItem(id, _));
EXPECT_CALL(wrapped_provider_, GetVisualsForItem_(id, _));
wrapped_provider_.SetItems(items);
provider_.OpenItem(LaunchLocation::DOWNLOAD_HOME, id);
provider_.RemoveItem(id);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment