// Copyright 2018 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/download/android/available_offline_content_provider.h"
#include <memory>
#include <utility>
#include "base/base64.h"
#include "base/functional/bind.h"
#include "base/memory/ref_counted_memory.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/strcat.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "chrome/browser/download/android/download_open_source.h"
#include "chrome/browser/download/android/download_utils.h"
#include "chrome/browser/offline_items_collection/offline_content_aggregator_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_key.h"
#include "components/feed/core/shared_prefs/pref_names.h"
#include "components/offline_items_collection/core/offline_content_aggregator.h"
#include "components/offline_items_collection/core/offline_content_provider.h"
#include "components/offline_items_collection/core/offline_item.h"
#include "components/offline_items_collection/core/offline_item_state.h"
#include "components/offline_pages/core/offline_page_feature.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/render_process_host.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "ui/base/l10n/time_format.h"
namespace android {
using chrome::mojom::AvailableContentType;
using GetVisualsOptions =
offline_items_collection::OfflineContentProvider::GetVisualsOptions;
using offline_items_collection::OfflineItem;
using offline_items_collection::OfflineItemState;
namespace {
// Minimum number of interesting offline items required to be available for any
// content card to be presented in the dino page.
const int kMinInterestingItemCount = 4;
// Maximum number of items that should be presented in the list of offline
// items.
const int kMaxListItemsToReturn = 3;
static_assert(
kMaxListItemsToReturn <= kMinInterestingItemCount,
"The number of items to list must be less or equal to the minimum number "
"of items that allow offline content to be presented");
// Returns a value that represents the priority of the content type.
// Smaller priority values are more important.
int ContentTypePriority(AvailableContentType type) {
switch (type) {
case AvailableContentType::kPrefetchedPage:
return 0;
case AvailableContentType::kVideo:
return 1;
case AvailableContentType::kAudio:
return 2;
case AvailableContentType::kOtherPage:
return 3;
case AvailableContentType::kUninteresting:
return 10000;
}
NOTREACHED_IN_MIGRATION();
}
AvailableContentType ContentType(const OfflineItem& item) {
// TODO(crbug.com/40111585): Make provider namespace a reusable constant.
if (item.is_transient || item.is_off_the_record ||
item.state != OfflineItemState::COMPLETE || item.is_dangerous ||
item.id.name_space == "content_index") {
return AvailableContentType::kUninteresting;
}
switch (item.filter) {
case offline_items_collection::FILTER_PAGE:
if (item.is_suggested)
return AvailableContentType::kPrefetchedPage;
return AvailableContentType::kOtherPage;
case offline_items_collection::FILTER_VIDEO:
return AvailableContentType::kVideo;
case offline_items_collection::FILTER_AUDIO:
return AvailableContentType::kAudio;
default:
break;
}
return AvailableContentType::kUninteresting;
}
bool CompareItemsByUsefulness(const OfflineItem& a, const OfflineItem& b) {
int a_priority = ContentTypePriority(ContentType(a));
int b_priority = ContentTypePriority(ContentType(b));
if (a_priority != b_priority)
return a_priority < b_priority;
// Break a tie by creation_time: more recent first.
if (a.creation_time != b.creation_time)
return a.creation_time > b.creation_time;
// Make sure only one ordering is possible.
return a.id < b.id;
}
class ThumbnailFetch {
public:
struct VisualsDataUris {
GURL thumbnail;
GURL favicon;
};
ThumbnailFetch(const ThumbnailFetch&) = delete;
ThumbnailFetch& operator=(const ThumbnailFetch&) = delete;
// Gets visuals for a list of visuals. Calls |complete_callback| with
// a list of VisualsDataUris structs containing data URIs for thumbnails and
// favicons for |content_ids|, in the same order. If no thumbnail or favicon
// 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<VisualsDataUris>)>
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<VisualsDataUris>)> complete_callback)
: content_ids_(std::move(content_ids)),
complete_callback_(std::move(complete_callback)) {
visuals_.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, GetVisualsOptions::IconAndCustomFavicon(), 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::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(std::move(complete_callback_), std::move(visuals_)));
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(
[](ThumbnailFetch* thumbnail_fetch) { delete thumbnail_fetch; },
this));
}
GURL GetImageAsDataUri(const gfx::Image& image) {
scoped_refptr<base::RefCountedMemory> data = image.As1xPNGBytes();
if (!data || data->size() == 0)
return GURL();
std::string png_base64 = base::Base64Encode(*data);
return GURL(base::StrCat({"data:image/png;base64,", png_base64}));
}
void AddVisual(
const offline_items_collection::ContentId& id,
std::unique_ptr<offline_items_collection::OfflineItemVisuals> visuals) {
if (!visuals)
return;
GURL thumbnail_data_uri = GetImageAsDataUri(visuals->icon);
GURL favicon_data_uri = GetImageAsDataUri(visuals->custom_favicon);
for (size_t i = 0; i < content_ids_.size(); ++i) {
if (content_ids_[i] == id) {
visuals_[i] = {std::move(thumbnail_data_uri),
std::move(favicon_data_uri)};
break;
}
}
}
// The list of item IDs for which to fetch visuals.
std::vector<offline_items_collection::ContentId> content_ids_;
// The thumbnail and favicon data URIs to be returned. |visuals_| size is
// equal to |content_ids_| size.
std::vector<VisualsDataUris> visuals_;
base::OnceCallback<void(std::vector<VisualsDataUris>)> complete_callback_;
size_t callback_count_ = 0;
};
chrome::mojom::AvailableOfflineContentPtr CreateAvailableOfflineContent(
const OfflineItem& item,
ThumbnailFetch::VisualsDataUris visuals_data_uris) {
return chrome::mojom::AvailableOfflineContent::New(
item.id.id, item.id.name_space, item.title, item.description,
base::UTF16ToUTF8(ui::TimeFormat::Simple(
ui::TimeFormat::FORMAT_ELAPSED, ui::TimeFormat::LENGTH_SHORT,
base::Time::Now() - item.creation_time)),
item.attribution, std::move(visuals_data_uris.thumbnail),
std::move(visuals_data_uris.favicon), ContentType(item));
}
} // namespace
AvailableOfflineContentProvider::AvailableOfflineContentProvider(
int render_process_host_id)
: render_process_host_id_(render_process_host_id) {}
AvailableOfflineContentProvider::~AvailableOfflineContentProvider() = default;
void AvailableOfflineContentProvider::List(ListCallback callback) {
Profile* profile = GetProfile();
if (!profile) {
CloseSelfOwnedReceiverIfNeeded();
return;
}
offline_items_collection::OfflineContentAggregator* aggregator =
OfflineContentAggregatorFactory::GetForKey(profile->GetProfileKey());
aggregator->GetAllItems(
base::BindOnce(&AvailableOfflineContentProvider::ListFinalize,
weak_ptr_factory_.GetWeakPtr(), std::move(callback),
base::Unretained(aggregator)));
}
void AvailableOfflineContentProvider::LaunchItem(
const std::string& item_id,
const std::string& name_space) {
Profile* profile = GetProfile();
if (!profile)
return;
offline_items_collection::OfflineContentAggregator* aggregator =
OfflineContentAggregatorFactory::GetForKey(profile->GetProfileKey());
offline_items_collection::OpenParams open_params(
offline_items_collection::LaunchLocation::NET_ERROR_SUGGESTION);
open_params.open_in_incognito = profile->IsOffTheRecord();
aggregator->OpenItem(
open_params, offline_items_collection::ContentId(name_space, item_id));
}
void AvailableOfflineContentProvider::LaunchDownloadsPage(
bool open_prefetched_articles_tab) {
DownloadUtils::ShowDownloadManager(
open_prefetched_articles_tab,
DownloadOpenSource::kDinoPageOfflineContent);
}
void AvailableOfflineContentProvider::ListVisibilityChanged(bool is_visible) {
Profile* profile = GetProfile();
if (!profile)
return;
profile->GetPrefs()->SetBoolean(feed::prefs::kArticlesListVisible,
is_visible);
}
// static
void AvailableOfflineContentProvider::Create(
int render_process_host_id,
mojo::PendingReceiver<chrome::mojom::AvailableOfflineContentProvider>
receiver) {
// Self owned receivers remain as long as the pipe is error free.
auto provider_self_owned_receiver = mojo::MakeSelfOwnedReceiver(
std::make_unique<AvailableOfflineContentProvider>(render_process_host_id),
std::move(receiver));
// TODO(curranmax): Rework this code so the static_cast is not needed.
auto* provider = static_cast<AvailableOfflineContentProvider*>(
provider_self_owned_receiver->impl());
provider->SetSelfOwnedReceiver(provider_self_owned_receiver);
}
// Picks the best available offline content items, and passes them to callback.
void AvailableOfflineContentProvider::ListFinalize(
AvailableOfflineContentProvider::ListCallback callback,
offline_items_collection::OfflineContentAggregator* aggregator,
const std::vector<OfflineItem>& all_items) {
Profile* profile = GetProfile();
if (!profile) {
CloseSelfOwnedReceiverIfNeeded();
return;
}
std::vector<OfflineItem> selected(kMinInterestingItemCount);
const auto end = std::partial_sort_copy(all_items.begin(), all_items.end(),
selected.begin(), selected.end(),
CompareItemsByUsefulness);
// If the number of interesting items is lower then the minimum don't show any
// suggestions. Otherwise trim it down to the number of expected items.
size_t copied_count = end - selected.begin();
DCHECK(copied_count <= kMinInterestingItemCount);
if (copied_count < kMinInterestingItemCount ||
ContentType(selected.back()) == AvailableContentType::kUninteresting) {
selected.clear();
} else {
selected.resize(kMaxListItemsToReturn);
}
std::vector<offline_items_collection::ContentId> selected_ids;
for (const OfflineItem& item : selected)
selected_ids.push_back(item.id);
bool list_visible_by_prefs =
profile->GetPrefs()->GetBoolean(feed::prefs::kArticlesListVisible);
auto complete =
[](AvailableOfflineContentProvider::ListCallback callback,
std::vector<OfflineItem> selected, bool list_visible_by_prefs,
std::vector<ThumbnailFetch::VisualsDataUris> visuals_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], std::move(visuals_data_uris[i])));
}
std::move(callback).Run(list_visible_by_prefs, std::move(result));
};
ThumbnailFetch::Start(
aggregator, selected_ids,
base::BindOnce(complete, std::move(callback), std::move(selected),
list_visible_by_prefs));
}
Profile* AvailableOfflineContentProvider::GetProfile() {
content::RenderProcessHost* render_process_host =
content::RenderProcessHost::FromID(render_process_host_id_);
if (!render_process_host)
return nullptr;
return Profile::FromBrowserContext(render_process_host->GetBrowserContext());
}
void AvailableOfflineContentProvider::SetSelfOwnedReceiver(
const mojo::SelfOwnedReceiverRef<
chrome::mojom::AvailableOfflineContentProvider>&
provider_self_owned_receiver) {
provider_self_owned_receiver_ = provider_self_owned_receiver;
}
void AvailableOfflineContentProvider::CloseSelfOwnedReceiverIfNeeded() {
// Closing the mojo pipe invalidates any pending callbacks, and they should
// not be used after the receiver is closed.
if (provider_self_owned_receiver_)
provider_self_owned_receiver_->Close();
}
} // namespace android