blob: 1445ce09b1018f7d16e268071ef0c8116b48f40c [file] [log] [blame]
// 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/offline_pages/core/downloads/download_ui_adapter.h"
#include <utility>
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/guid.h"
#include "base/logging.h"
#include "base/metrics/histogram_macros.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/trace_event/trace_event.h"
#include "components/offline_items_collection/core/fail_state.h"
#include "components/offline_pages/core/background/request_coordinator.h"
#include "components/offline_pages/core/background/request_notifier.h"
#include "components/offline_pages/core/background/save_page_request.h"
#include "components/offline_pages/core/client_namespace_constants.h"
#include "components/offline_pages/core/client_policy_controller.h"
#include "components/offline_pages/core/downloads/offline_item_conversions.h"
#include "components/offline_pages/core/offline_page_model.h"
#include "components/offline_pages/core/page_criteria.h"
#include "components/offline_pages/core/thumbnail_decoder.h"
#include "ui/gfx/image/image.h"
namespace {
// Value of this constant doesn't matter, only its address is used.
const char kDownloadUIAdapterKey[] = "";
} // namespace
namespace offline_pages {
namespace {
bool RequestsMatchesGuid(const std::string& guid,
ClientPolicyController* policy_controller,
const SavePageRequest& request) {
return request.client_id().id == guid &&
policy_controller->IsSupportedByDownload(
request.client_id().name_space);
}
std::vector<int64_t> FilterRequestsByGuid(
std::vector<std::unique_ptr<SavePageRequest>> requests,
const std::string& guid,
ClientPolicyController* policy_controller) {
std::vector<int64_t> request_ids;
for (const auto& request : requests) {
if (RequestsMatchesGuid(guid, policy_controller, *request))
request_ids.push_back(request->request_id());
}
return request_ids;
}
} // namespace
// static
DownloadUIAdapter* DownloadUIAdapter::FromOfflinePageModel(
OfflinePageModel* model) {
DCHECK(model);
return static_cast<DownloadUIAdapter*>(
model->GetUserData(kDownloadUIAdapterKey));
}
// static
void DownloadUIAdapter::AttachToOfflinePageModel(
std::unique_ptr<DownloadUIAdapter> adapter,
OfflinePageModel* model) {
DCHECK(adapter);
DCHECK(model);
model->SetUserData(kDownloadUIAdapterKey, std::move(adapter));
}
DownloadUIAdapter::DownloadUIAdapter(
OfflineContentAggregator* aggregator,
OfflinePageModel* model,
RequestCoordinator* request_coordinator,
std::unique_ptr<ThumbnailDecoder> thumbnail_decoder,
std::unique_ptr<Delegate> delegate)
: aggregator_(aggregator),
model_(model),
request_coordinator_(request_coordinator),
thumbnail_decoder_(std::move(thumbnail_decoder)),
delegate_(std::move(delegate)),
weak_ptr_factory_(this) {
delegate_->SetUIAdapter(this);
if (aggregator_)
aggregator_->RegisterProvider(kOfflinePageNamespace, this);
if (model_)
model_->AddObserver(this);
if (request_coordinator_)
request_coordinator_->AddObserver(this);
}
DownloadUIAdapter::~DownloadUIAdapter() {
if (aggregator_)
aggregator_->UnregisterProvider(kOfflinePageNamespace);
}
void DownloadUIAdapter::AddObserver(
OfflineContentProvider::Observer* observer) {
DCHECK(observer);
if (observers_.HasObserver(observer))
return;
observers_.AddObserver(observer);
}
void DownloadUIAdapter::RemoveObserver(
OfflineContentProvider::Observer* observer) {
DCHECK(observer);
if (!observers_.HasObserver(observer))
return;
observers_.RemoveObserver(observer);
}
void DownloadUIAdapter::OfflinePageModelLoaded(OfflinePageModel* model) {
// This signal is not used here.
}
// OfflinePageModel::Observer
void DownloadUIAdapter::OfflinePageAdded(OfflinePageModel* model,
const OfflinePageItem& added_page) {
DCHECK(model == model_);
if (!delegate_->IsVisibleInUI(added_page.client_id))
return;
bool is_suggested = model->GetPolicyController()->IsSuggested(
added_page.client_id.name_space);
OfflineItem offline_item(
OfflineItemConversions::CreateOfflineItem(added_page, is_suggested));
// We assume the pages which are non-suggested and shown in Download Home UI
// should be coming from requests, so their corresponding offline items should
// have been added to the UI when their corresponding requests were created.
// So OnItemUpdated is used for non-suggested pages.
// Otherwise, for pages of suggested articles, they'll be added to the UI
// since they're added to Offline Page database directly, so OnItemsAdded is
// used.
for (auto& observer : observers_) {
if (!is_suggested)
observer.OnItemUpdated(offline_item);
else
observer.OnItemsAdded({offline_item});
}
}
// OfflinePageModel::Observer
void DownloadUIAdapter::OfflinePageDeleted(
const OfflinePageModel::DeletedPageInfo& page_info) {
if (!delegate_->IsVisibleInUI(page_info.client_id))
return;
for (auto& observer : observers_) {
observer.OnItemRemoved(
ContentId(kOfflinePageNamespace, page_info.client_id.id));
}
}
// OfflinePageModel::Observer
void DownloadUIAdapter::ThumbnailAdded(OfflinePageModel* model,
const int64_t offline_id,
const std::string& thumbnail) {
model_->GetPageByOfflineId(
offline_id, base::BindOnce(&DownloadUIAdapter::OnPageGetForThumbnailAdded,
weak_ptr_factory_.GetWeakPtr()));
}
// RequestCoordinator::Observer
void DownloadUIAdapter::OnAdded(const SavePageRequest& added_request) {
if (!delegate_->IsVisibleInUI(added_request.client_id()))
return;
OfflineItem offline_item(
OfflineItemConversions::CreateOfflineItem(added_request));
for (auto& observer : observers_)
observer.OnItemsAdded({offline_item});
}
// RequestCoordinator::Observer
void DownloadUIAdapter::OnCompleted(
const SavePageRequest& request,
RequestNotifier::BackgroundSavePageResult status) {
if (!delegate_->IsVisibleInUI(request.client_id()))
return;
if (delegate_->MaybeSuppressNotification(request.request_origin(),
request.client_id())) {
return;
}
OfflineItem item = OfflineItemConversions::CreateOfflineItem(request);
if (status == RequestNotifier::BackgroundSavePageResult::SUCCESS) {
// If the request is completed successfully, it means there should already
// be a OfflinePageAdded fired. So doing nothing in this case.
} else if (status ==
RequestNotifier::BackgroundSavePageResult::USER_CANCELED ||
status == RequestNotifier::BackgroundSavePageResult::
DOWNLOAD_THROTTLED) {
for (auto& observer : observers_)
observer.OnItemRemoved(item.id);
} else {
item.state = offline_items_collection::OfflineItemState::FAILED;
// Actual cause could be server or network related, but we need to pick
// a fail_state.
item.fail_state = offline_items_collection::FailState::SERVER_FAILED;
for (auto& observer : observers_)
observer.OnItemUpdated(item);
}
}
// RequestCoordinator::Observer
void DownloadUIAdapter::OnChanged(const SavePageRequest& request) {
if (!delegate_->IsVisibleInUI(request.client_id()))
return;
OfflineItem offline_item(OfflineItemConversions::CreateOfflineItem(request));
for (OfflineContentProvider::Observer& observer : observers_)
observer.OnItemUpdated(offline_item);
}
// RequestCoordinator::Observer
void DownloadUIAdapter::OnNetworkProgress(const SavePageRequest& request,
int64_t received_bytes) {
if (!delegate_->IsVisibleInUI(request.client_id()))
return;
OfflineItem offline_item(OfflineItemConversions::CreateOfflineItem(request));
offline_item.received_bytes = received_bytes;
for (auto& observer : observers_)
observer.OnItemUpdated(offline_item);
}
void DownloadUIAdapter::GetAllItems(
OfflineContentProvider::MultipleItemCallback callback) {
std::unique_ptr<OfflineContentProvider::OfflineItemList> offline_items =
std::make_unique<OfflineContentProvider::OfflineItemList>();
model_->GetAllPages(base::BindOnce(
&DownloadUIAdapter::OnOfflinePagesLoaded, weak_ptr_factory_.GetWeakPtr(),
std::move(callback), std::move(offline_items)));
}
void DownloadUIAdapter::GetVisualsForItem(const ContentId& id,
VisualsCallback visuals_callback) {
PageCriteria criteria;
criteria.guid = id.id;
criteria.maximum_matches = 1;
model_->GetPagesWithCriteria(
criteria, base::BindOnce(&DownloadUIAdapter::OnPageGetForVisuals,
weak_ptr_factory_.GetWeakPtr(), id,
std::move(visuals_callback)));
}
void DownloadUIAdapter::GetShareInfoForItem(const ContentId& id,
ShareCallback share_callback) {
delegate_->GetShareInfoForItem(id, std::move(share_callback));
}
void DownloadUIAdapter::RenameItem(const ContentId& id,
const std::string& name,
RenameCallback callback) {
NOTREACHED();
}
void DownloadUIAdapter::OnPageGetForVisuals(
const ContentId& id,
VisualsCallback visuals_callback,
const std::vector<OfflinePageItem>& pages) {
if (pages.empty()) {
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(std::move(visuals_callback), id, nullptr));
return;
}
const OfflinePageItem* page = &pages[0];
VisualResultCallback callback =
base::BindOnce(std::move(visuals_callback), id);
if (page->client_id.name_space == kSuggestedArticlesNamespace) {
// Report PrefetchedItemHasThumbnail along with result callback.
auto report_and_callback =
[](VisualResultCallback result_callback,
std::unique_ptr<offline_items_collection::OfflineItemVisuals>
visuals) {
UMA_HISTOGRAM_BOOLEAN(
"OfflinePages.DownloadUI.PrefetchedItemHasThumbnail",
visuals != nullptr);
std::move(result_callback).Run(std::move(visuals));
};
callback = base::BindOnce(report_and_callback, std::move(callback));
}
model_->GetVisualsByOfflineId(
page->offline_id,
base::BindOnce(&DownloadUIAdapter::OnVisualsLoaded,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void DownloadUIAdapter::OnVisualsLoaded(
VisualResultCallback callback,
std::unique_ptr<OfflinePageVisuals> visuals) {
DCHECK(thumbnail_decoder_);
if (!visuals || visuals->thumbnail.empty()) {
// PostTask not required, GetThumbnailByOfflineId does it for us.
std::move(callback).Run(nullptr);
return;
}
auto forward_visuals_lambda = [](VisualResultCallback callback,
const gfx::Image& image) {
if (image.IsEmpty()) {
std::move(callback).Run(nullptr);
return;
}
auto visuals =
std::make_unique<offline_items_collection::OfflineItemVisuals>();
visuals->icon = image;
std::move(callback).Run(std::move(visuals));
};
thumbnail_decoder_->DecodeAndCropThumbnail(
visuals->thumbnail,
base::BindOnce(forward_visuals_lambda, std::move(callback)));
}
void DownloadUIAdapter::OnPageGetForThumbnailAdded(
const OfflinePageItem* page) {
if (!page)
return;
bool is_suggested =
model_->GetPolicyController()->IsSuggested(page->client_id.name_space);
for (auto& observer : observers_)
observer.OnItemUpdated(
OfflineItemConversions::CreateOfflineItem(*page, is_suggested));
}
// TODO(dimich): Remove this method since it is not used currently. If needed,
// it has to be updated to fault in the initial load of items. Currently it
// simply returns nullopt if the cache is not loaded.
void DownloadUIAdapter::GetItemById(
const ContentId& id,
OfflineContentProvider::SingleItemCallback callback) {
PageCriteria criteria;
criteria.guid = id.id;
criteria.maximum_matches = 1;
model_->GetPagesWithCriteria(
criteria,
base::BindOnce(&DownloadUIAdapter::OnPageGetForGetItem,
weak_ptr_factory_.GetWeakPtr(), id, std::move(callback)));
}
void DownloadUIAdapter::OnPageGetForGetItem(
const ContentId& id,
OfflineContentProvider::SingleItemCallback callback,
const std::vector<OfflinePageItem>& pages) {
if (!pages.empty()) {
const OfflinePageItem* page = &pages[0];
bool is_suggested =
model_->GetPolicyController()->IsSuggested(page->client_id.name_space);
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback),
OfflineItemConversions::CreateOfflineItem(
*page, is_suggested)));
return;
}
request_coordinator_->GetAllRequests(
base::BindOnce(&DownloadUIAdapter::OnAllRequestsGetForGetItem,
weak_ptr_factory_.GetWeakPtr(), id, std::move(callback)));
}
void DownloadUIAdapter::OnAllRequestsGetForGetItem(
const ContentId& id,
OfflineContentProvider::SingleItemCallback callback,
std::vector<std::unique_ptr<SavePageRequest>> requests) {
base::Optional<OfflineItem> offline_item;
for (const auto& request : requests) {
if (request->client_id().id == id.id)
offline_item = OfflineItemConversions::CreateOfflineItem(*request);
}
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), offline_item));
}
void DownloadUIAdapter::OpenItem(LaunchLocation location, const ContentId& id) {
PageCriteria criteria;
criteria.guid = id.id;
criteria.maximum_matches = 1;
model_->GetPagesWithCriteria(
criteria, base::BindOnce(&DownloadUIAdapter::OnPageGetForOpenItem,
weak_ptr_factory_.GetWeakPtr(), location));
}
void DownloadUIAdapter::OnPageGetForOpenItem(
LaunchLocation location,
const std::vector<OfflinePageItem>& pages) {
if (pages.empty())
return;
const OfflinePageItem* page = &pages[0];
bool is_suggested =
model_->GetPolicyController()->IsSuggested(page->client_id.name_space);
OfflineItem item =
OfflineItemConversions::CreateOfflineItem(*page, is_suggested);
delegate_->OpenItem(item, page->offline_id, location);
}
void DownloadUIAdapter::RemoveItem(const ContentId& id) {
std::vector<ClientId> client_ids;
auto* policy_controller = model_->GetPolicyController();
for (const auto& name_space :
policy_controller->GetNamespacesSupportedByDownload()) {
client_ids.push_back(ClientId(name_space, id.id));
}
model_->DeletePagesByClientIds(
client_ids, base::BindRepeating(&DownloadUIAdapter::OnDeletePagesDone,
weak_ptr_factory_.GetWeakPtr()));
}
void DownloadUIAdapter::CancelDownload(const ContentId& id) {
auto predicate =
base::BindRepeating(&RequestsMatchesGuid, id.id,
// Since RequestCoordinator is calling us back,
// binding its policy controller is safe.
request_coordinator_->GetPolicyController());
request_coordinator_->RemoveRequestsIf(predicate, base::DoNothing());
}
void DownloadUIAdapter::PauseDownload(const ContentId& id) {
// TODO(fgorski): Clean this up in a way where 2 round trips + GetAllRequests
// is not necessary.
request_coordinator_->GetAllRequests(
base::BindOnce(&DownloadUIAdapter::PauseDownloadContinuation,
weak_ptr_factory_.GetWeakPtr(), id.id));
}
void DownloadUIAdapter::PauseDownloadContinuation(
const std::string& guid,
std::vector<std::unique_ptr<SavePageRequest>> requests) {
request_coordinator_->PauseRequests(FilterRequestsByGuid(
std::move(requests), guid, request_coordinator_->GetPolicyController()));
}
void DownloadUIAdapter::ResumeDownload(const ContentId& id,
bool has_user_gesture) {
// TODO(fgorski): Clean this up in a way where 2 round trips + GetAllRequests
// is not necessary.
if (has_user_gesture) {
request_coordinator_->GetAllRequests(
base::BindOnce(&DownloadUIAdapter::ResumeDownloadContinuation,
weak_ptr_factory_.GetWeakPtr(), id.id));
} else {
request_coordinator_->StartImmediateProcessing(base::DoNothing());
}
}
void DownloadUIAdapter::ResumeDownloadContinuation(
const std::string& guid,
std::vector<std::unique_ptr<SavePageRequest>> requests) {
request_coordinator_->ResumeRequests(FilterRequestsByGuid(
std::move(requests), guid, request_coordinator_->GetPolicyController()));
}
void DownloadUIAdapter::OnOfflinePagesLoaded(
OfflineContentProvider::MultipleItemCallback callback,
std::unique_ptr<OfflineContentProvider::OfflineItemList> offline_items,
const MultipleOfflinePageItemResult& pages) {
for (const auto& page : pages) {
if (delegate_->IsVisibleInUI(page.client_id)) {
std::string guid = page.client_id.id;
bool is_suggested =
model_->GetPolicyController()->IsSuggested(page.client_id.name_space);
offline_items->push_back(
OfflineItemConversions::CreateOfflineItem(page, is_suggested));
}
}
request_coordinator_->GetAllRequests(base::BindOnce(
&DownloadUIAdapter::OnRequestsLoaded, weak_ptr_factory_.GetWeakPtr(),
std::move(callback), std::move(offline_items)));
}
void DownloadUIAdapter::OnRequestsLoaded(
OfflineContentProvider::MultipleItemCallback callback,
std::unique_ptr<OfflineContentProvider::OfflineItemList> offline_items,
std::vector<std::unique_ptr<SavePageRequest>> requests) {
for (const auto& request : requests) {
if (delegate_->IsVisibleInUI(request->client_id())) {
std::string guid = request->client_id().id;
offline_items->push_back(
OfflineItemConversions::CreateOfflineItem(*request.get()));
}
}
OfflineContentProvider::OfflineItemList list = *offline_items;
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), list));
}
void DownloadUIAdapter::OnDeletePagesDone(DeletePageResult result) {
// TODO(dimich): Consider adding UMA to record user actions.
}
} // namespace offline_pages