blob: d25348e5cb423e932f122db8218bb1cb7fb23703 [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 "chrome/browser/background_fetch/background_fetch_delegate_impl.h"
#include <utility>
#include "base/bind.h"
#include "base/guid.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "chrome/browser/download/download_service_factory.h"
#include "chrome/browser/offline_items_collection/offline_content_aggregator_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "components/download/public/download_params.h"
#include "components/download/public/download_service.h"
#include "components/offline_items_collection/core/offline_content_aggregator.h"
#include "components/offline_items_collection/core/offline_item.h"
#include "content/public/browser/background_fetch_response.h"
#include "content/public/browser/browser_thread.h"
BackgroundFetchDelegateImpl::BackgroundFetchDelegateImpl(Profile* profile)
: download_service_(
DownloadServiceFactory::GetInstance()->GetForBrowserContext(profile)),
offline_content_aggregator_(
offline_items_collection::OfflineContentAggregatorFactory::
GetForBrowserContext(profile)),
weak_ptr_factory_(this) {
offline_content_aggregator_->RegisterProvider("background_fetch", this);
}
BackgroundFetchDelegateImpl::~BackgroundFetchDelegateImpl() {}
void BackgroundFetchDelegateImpl::Shutdown() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (client()) {
client()->OnDelegateShutdown();
}
}
BackgroundFetchDelegateImpl::JobDetails::JobDetails(JobDetails&&) = default;
BackgroundFetchDelegateImpl::JobDetails::JobDetails(
const std::string& job_unique_id,
const std::string& title,
const url::Origin& origin,
int completed_parts,
int total_parts)
: title(title),
origin(origin),
completed_parts(completed_parts),
total_parts(total_parts),
cancelled(false),
offline_item(offline_items_collection::ContentId("background_fetch",
job_unique_id)) {
UpdateOfflineItem();
}
BackgroundFetchDelegateImpl::JobDetails::~JobDetails() = default;
void BackgroundFetchDelegateImpl::JobDetails::UpdateOfflineItem() {
if (total_parts > 0) {
offline_item.progress.value = completed_parts;
offline_item.progress.max = total_parts;
offline_item.progress.unit =
offline_items_collection::OfflineItemProgressUnit::PERCENTAGE;
}
if (title.empty()) {
offline_item.title = origin.Serialize();
} else {
// TODO(crbug.com/774612): Make sure that the origin is displayed completely
// in all cases so that long titles cannot obscure it.
offline_item.title = base::StringPrintf("%s (%s)", title.c_str(),
origin.Serialize().c_str());
}
// TODO(delphick): Figure out what to put in offline_item.description.
offline_item.is_transient = true;
using OfflineItemState = offline_items_collection::OfflineItemState;
if (cancelled)
offline_item.state = OfflineItemState::CANCELLED;
else if (completed_parts == total_parts)
offline_item.state = OfflineItemState::COMPLETE;
else
offline_item.state = OfflineItemState::IN_PROGRESS;
}
void BackgroundFetchDelegateImpl::CreateDownloadJob(
const std::string& job_unique_id,
const std::string& title,
const url::Origin& origin,
int completed_parts,
int total_parts,
const std::vector<std::string>& current_guids) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(!job_details_map_.count(job_unique_id));
auto emplace_result = job_details_map_.emplace(
job_unique_id,
JobDetails(job_unique_id, title, origin, completed_parts, total_parts));
const JobDetails& details = emplace_result.first->second;
for (const auto& download_guid : current_guids) {
DCHECK(!download_job_unique_id_map_.count(download_guid));
download_job_unique_id_map_.emplace(download_guid, job_unique_id);
}
for (auto* observer : observers_) {
observer->OnItemsAdded({details.offline_item});
}
}
void BackgroundFetchDelegateImpl::DownloadUrl(
const std::string& job_unique_id,
const std::string& download_guid,
const std::string& method,
const GURL& url,
const net::NetworkTrafficAnnotationTag& traffic_annotation,
const net::HttpRequestHeaders& headers) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(job_details_map_.count(job_unique_id));
DCHECK(!download_job_unique_id_map_.count(download_guid));
JobDetails& job_details = job_details_map_.find(job_unique_id)->second;
job_details.current_download_guids.insert(download_guid);
download_job_unique_id_map_.emplace(download_guid, job_unique_id);
download::DownloadParams params;
params.guid = download_guid;
params.client = download::DownloadClient::BACKGROUND_FETCH;
params.request_params.method = method;
params.request_params.url = url;
params.request_params.request_headers = headers;
params.callback = base::Bind(&BackgroundFetchDelegateImpl::OnDownloadReceived,
weak_ptr_factory_.GetWeakPtr());
params.traffic_annotation =
net::MutableNetworkTrafficAnnotationTag(traffic_annotation);
download_service_->StartDownload(params);
}
void BackgroundFetchDelegateImpl::Abort(const std::string& job_unique_id) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
auto job_details_iter = job_details_map_.find(job_unique_id);
if (job_details_iter == job_details_map_.end())
return;
JobDetails& job_details = job_details_iter->second;
job_details.cancelled = true;
UpdateOfflineItemAndUpdateObservers(&job_details);
for (const auto& download_guid : job_details.current_download_guids) {
download_service_->CancelDownload(download_guid);
download_job_unique_id_map_.erase(download_guid);
}
job_details_map_.erase(job_details_iter);
}
void BackgroundFetchDelegateImpl::OnDownloadStarted(
const std::string& download_guid,
std::unique_ptr<content::BackgroundFetchResponse> response) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
auto download_job_unique_id_iter =
download_job_unique_id_map_.find(download_guid);
// TODO(crbug.com/779012): When DownloadService fixes cancelled jobs calling
// OnDownload* methods, then this can be a DCHECK.
if (download_job_unique_id_iter == download_job_unique_id_map_.end())
return;
const std::string& job_unique_id = download_job_unique_id_iter->second;
if (client()) {
client()->OnDownloadStarted(job_unique_id, download_guid,
std::move(response));
}
}
void BackgroundFetchDelegateImpl::OnDownloadUpdated(
const std::string& download_guid,
uint64_t bytes_downloaded) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
auto download_job_unique_id_iter =
download_job_unique_id_map_.find(download_guid);
// TODO(crbug.com/779012): When DownloadService fixes cancelled jobs calling
// OnDownload* methods, then this can be a DCHECK.
if (download_job_unique_id_iter == download_job_unique_id_map_.end())
return;
const std::string& job_unique_id = download_job_unique_id_iter->second;
if (client())
client()->OnDownloadUpdated(job_unique_id, download_guid, bytes_downloaded);
}
void BackgroundFetchDelegateImpl::OnDownloadFailed(
const std::string& download_guid,
download::Client::FailureReason reason) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
using FailureReason = content::BackgroundFetchResult::FailureReason;
FailureReason failure_reason;
auto download_job_unique_id_iter =
download_job_unique_id_map_.find(download_guid);
// TODO(crbug.com/779012): When DownloadService fixes cancelled jobs
// potentially calling OnDownloadFailed with a reason other than
// CANCELLED/ABORTED, we should add a DCHECK here.
if (download_job_unique_id_iter == download_job_unique_id_map_.end())
return;
const std::string& job_unique_id = download_job_unique_id_iter->second;
JobDetails& job_details = job_details_map_.find(job_unique_id)->second;
++job_details.completed_parts;
UpdateOfflineItemAndUpdateObservers(&job_details);
switch (reason) {
case download::Client::FailureReason::NETWORK:
failure_reason = FailureReason::NETWORK;
break;
case download::Client::FailureReason::TIMEDOUT:
failure_reason = FailureReason::TIMEDOUT;
break;
case download::Client::FailureReason::UNKNOWN:
failure_reason = FailureReason::UNKNOWN;
break;
case download::Client::FailureReason::ABORTED:
case download::Client::FailureReason::CANCELLED:
// The client cancelled or aborted it so no need to notify it.
return;
default:
NOTREACHED();
return;
}
// TODO(delphick): consider calling OnItemUpdated here as well if for instance
// the download actually happened but 404ed.
if (client()) {
client()->OnDownloadComplete(
job_unique_id, download_guid,
std::make_unique<content::BackgroundFetchResult>(base::Time::Now(),
failure_reason));
}
job_details.current_download_guids.erase(
job_details.current_download_guids.find(download_guid));
download_job_unique_id_map_.erase(download_guid);
}
void BackgroundFetchDelegateImpl::OnDownloadSucceeded(
const std::string& download_guid,
const base::FilePath& path,
uint64_t size) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
auto download_job_unique_id_iter =
download_job_unique_id_map_.find(download_guid);
// TODO(crbug.com/779012): When DownloadService fixes cancelled jobs calling
// OnDownload* methods, then this can be a DCHECK.
if (download_job_unique_id_iter == download_job_unique_id_map_.end())
return;
const std::string& job_unique_id = download_job_unique_id_iter->second;
JobDetails& job_details = job_details_map_.find(job_unique_id)->second;
++job_details.completed_parts;
UpdateOfflineItemAndUpdateObservers(&job_details);
if (client()) {
client()->OnDownloadComplete(
job_unique_id, download_guid,
std::make_unique<content::BackgroundFetchResult>(base::Time::Now(),
path, size));
}
job_details.current_download_guids.erase(
job_details.current_download_guids.find(download_guid));
download_job_unique_id_map_.erase(download_guid);
}
void BackgroundFetchDelegateImpl::OnDownloadReceived(
const std::string& download_guid,
download::DownloadParams::StartResult result) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
using StartResult = download::DownloadParams::StartResult;
switch (result) {
case StartResult::ACCEPTED:
// Nothing to do.
break;
case StartResult::BACKOFF:
// TODO(delphick): try again later?
NOTREACHED();
break;
case StartResult::UNEXPECTED_CLIENT:
// This really should never happen since we're supplying the
// DownloadClient.
NOTREACHED();
case StartResult::UNEXPECTED_GUID:
// TODO(delphick): try again with a different GUID.
NOTREACHED();
case StartResult::CLIENT_CANCELLED:
// TODO(delphick): do we need to do anything here, since we will have
// cancelled it?
break;
case StartResult::INTERNAL_ERROR:
// TODO(delphick): We need to handle this gracefully.
NOTREACHED();
case StartResult::COUNT:
NOTREACHED();
}
}
// Much of the code in offline_item_collection is not re-entrant, so this should
// not be called from any of the OfflineContentProvider-inherited methods.
void BackgroundFetchDelegateImpl::UpdateOfflineItemAndUpdateObservers(
JobDetails* job_details) {
job_details->UpdateOfflineItem();
for (auto* observer : observers_)
observer->OnItemUpdated(job_details->offline_item);
}
bool BackgroundFetchDelegateImpl::AreItemsAvailable() {
return true;
}
void BackgroundFetchDelegateImpl::OpenItem(
const offline_items_collection::ContentId& id) {
// TODO(delphick): Add custom OpenItem behavior.
NOTIMPLEMENTED();
}
void BackgroundFetchDelegateImpl::RemoveItem(
const offline_items_collection::ContentId& id) {
// TODO(delphick): Support removing items. (Not sure when this would actually
// get called though).
NOTIMPLEMENTED();
}
void BackgroundFetchDelegateImpl::CancelDownload(
const offline_items_collection::ContentId& id) {
auto job_details_iter = job_details_map_.find(id.id);
if (job_details_iter == job_details_map_.end())
return;
JobDetails& job_details = job_details_iter->second;
for (auto& download_guid : job_details.current_download_guids) {
download_service_->CancelDownload(download_guid);
download_job_unique_id_map_.erase(download_guid);
}
if (client())
client()->OnJobCancelled(id.id);
job_details_map_.erase(job_details_iter);
}
void BackgroundFetchDelegateImpl::PauseDownload(
const offline_items_collection::ContentId& id) {
auto job_details_iter = job_details_map_.find(id.id);
if (job_details_iter == job_details_map_.end())
return;
JobDetails& job_details = job_details_iter->second;
for (auto& download_guid : job_details.current_download_guids)
download_service_->PauseDownload(download_guid);
// TODO(delphick): Mark overall download job as paused so that future
// downloads are not started until resume. (Initially not a worry because only
// one download will be scheduled at a time).
}
void BackgroundFetchDelegateImpl::ResumeDownload(
const offline_items_collection::ContentId& id,
bool has_user_gesture) {
auto job_details_iter = job_details_map_.find(id.id);
if (job_details_iter == job_details_map_.end())
return;
JobDetails& job_details = job_details_iter->second;
for (auto& download_guid : job_details.current_download_guids)
download_service_->ResumeDownload(download_guid);
// TODO(delphick): Start new downloads that weren't started because of pause.
}
const offline_items_collection::OfflineItem*
BackgroundFetchDelegateImpl::GetItemById(
const offline_items_collection::ContentId& id) {
auto it = job_details_map_.find(id.id);
return (it != job_details_map_.end()) ? &it->second.offline_item : nullptr;
}
BackgroundFetchDelegateImpl::OfflineItemList
BackgroundFetchDelegateImpl::GetAllItems() {
OfflineItemList item_list;
for (auto& entry : job_details_map_)
item_list.push_back(entry.second.offline_item);
return item_list;
}
void BackgroundFetchDelegateImpl::GetVisualsForItem(
const offline_items_collection::ContentId& id,
const VisualsCallback& callback) {
// GetVisualsForItem mustn't be called directly since offline_items_collection
// is not re-entrant and it must be called even if there are no visuals.
// TODO(delphick): Call with an image when that becomes available.
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(callback, id, nullptr));
}
void BackgroundFetchDelegateImpl::AddObserver(Observer* observer) {
DCHECK(!observers_.count(observer));
observers_.insert(observer);
// OnItemsAvailable mustn't be called directly since offline_items_collection
// is not re-entrant.
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(
[](Observer* observer,
base::WeakPtr<BackgroundFetchDelegateImpl> provider) {
if (provider)
observer->OnItemsAvailable(provider.get());
},
observer, weak_ptr_factory_.GetWeakPtr()));
}
void BackgroundFetchDelegateImpl::RemoveObserver(Observer* observer) {
observers_.erase(observer);
}