// Copyright 2021 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/background_fetch/background_fetch_delegate_base.h" #include #include "base/bind.h" #include "base/callback_helpers.h" #include "base/check_op.h" #include "base/notreached.h" #include "build/build_config.h" #include "components/background_fetch/job_details.h" #include "components/content_settings/core/common/content_settings_types.h" #include "components/download/public/background_service/background_download_service.h" #include "components/download/public/background_service/blob_context_getter_factory.h" #include "components/download/public/background_service/download_params.h" #include "content/public/browser/background_fetch_description.h" #include "content/public/browser/background_fetch_response.h" #include "content/public/browser/browser_context.h" #include "content/public/browser/browser_thread.h" #include "content/public/browser/download_manager.h" #include "content/public/browser/download_manager_delegate.h" #include "content/public/browser/web_contents.h" #include "mojo/public/cpp/bindings/pending_remote.h" #include "mojo/public/cpp/bindings/remote.h" #include "services/network/public/mojom/data_pipe_getter.mojom.h" #include "third_party/blink/public/mojom/background_fetch/background_fetch.mojom.h" #include "third_party/blink/public/mojom/blob/blob.mojom.h" #include "ui/gfx/geometry/size.h" namespace background_fetch { BackgroundFetchDelegateBase::BackgroundFetchDelegateBase( content::BrowserContext* context) : context_(context) {} BackgroundFetchDelegateBase::~BackgroundFetchDelegateBase() = default; void BackgroundFetchDelegateBase::GetIconDisplaySize( BackgroundFetchDelegate::GetIconDisplaySizeCallback callback) { DCHECK_CURRENTLY_ON(content::BrowserThread::UI); // If Android, return 192x192, else return 0x0. 0x0 means not loading an // icon at all, which is returned for all non-Android platforms as the // icons can't be displayed on the UI yet. gfx::Size display_size; #if BUILDFLAG(IS_ANDROID) display_size = gfx::Size(192, 192); #endif std::move(callback).Run(display_size); } void BackgroundFetchDelegateBase::CreateDownloadJob( base::WeakPtr client, std::unique_ptr fetch_description) { DCHECK_CURRENTLY_ON(content::BrowserThread::UI); const std::string job_id = fetch_description->job_unique_id; auto inserted = job_details_map_.emplace(std::piecewise_construct, std::forward_as_tuple(job_id), std::forward_as_tuple()); DCHECK(inserted.second); JobDetails* job_details = &inserted.first->second; job_details->client = std::move(client); job_details->job_state = fetch_description->start_paused ? JobDetails::State::kPendingWillStartPaused : JobDetails::State::kPendingWillStartDownloading; job_details->fetch_description = std::move(fetch_description); OnJobDetailsCreated(job_id); } void BackgroundFetchDelegateBase::DownloadUrl( const std::string& job_id, const std::string& download_guid, const std::string& method, const GURL& url, ::network::mojom::CredentialsMode credentials_mode, const net::NetworkTrafficAnnotationTag& traffic_annotation, const net::HttpRequestHeaders& headers, bool has_request_body) { DCHECK_CURRENTLY_ON(content::BrowserThread::UI); DCHECK(!download_job_id_map_.count(download_guid)); download_job_id_map_.emplace(download_guid, job_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.request_params.credentials_mode = credentials_mode; params.callback = base::BindRepeating(&BackgroundFetchDelegateBase::OnDownloadReceived, weak_ptr_factory_.GetWeakPtr()); params.traffic_annotation = net::MutableNetworkTrafficAnnotationTag(traffic_annotation); params.request_params.update_first_party_url_on_redirect = false; JobDetails* job_details = GetJobDetails(job_id); if (job_details->job_state == JobDetails::State::kPendingWillStartPaused || job_details->job_state == JobDetails::State::kPendingWillStartDownloading) { DoShowUi(job_id); job_details->MarkJobAsStarted(); } params.request_params.isolation_info = job_details->fetch_description->isolation_info; if (job_details->job_state == JobDetails::State::kStartedButPaused) { job_details->on_resume = base::BindOnce( &BackgroundFetchDelegateBase::StartDownload, GetWeakPtr(), job_id, std::move(params), has_request_body); } else { StartDownload(job_id, std::move(params), has_request_body); } DoUpdateUi(job_id); } void BackgroundFetchDelegateBase::PauseDownload(const std::string& job_id) { DCHECK_CURRENTLY_ON(content::BrowserThread::UI); JobDetails* job_details = GetJobDetails(job_id, /*allow_null=*/true); if (!job_details) return; if (job_details->job_state == JobDetails::State::kDownloadsComplete || job_details->job_state == JobDetails::State::kJobComplete) { // The pause event arrived after the fetch was complete; ignore it. return; } job_details->job_state = JobDetails::State::kStartedButPaused; for (auto& download_guid_pair : job_details->current_fetch_guids) GetDownloadService()->PauseDownload(download_guid_pair.first); } void BackgroundFetchDelegateBase::ResumeDownload(const std::string& job_id) { DCHECK_CURRENTLY_ON(content::BrowserThread::UI); JobDetails* job_details = GetJobDetails(job_id, /*allow_null=*/true); if (!job_details) return; job_details->job_state = JobDetails::State::kStartedAndDownloading; for (auto& download_guid_pair : job_details->current_fetch_guids) GetDownloadService()->ResumeDownload(download_guid_pair.first); if (job_details->on_resume) std::move(job_details->on_resume).Run(); } void BackgroundFetchDelegateBase::CancelDownload(std::string job_id) { DCHECK_CURRENTLY_ON(content::BrowserThread::UI); JobDetails* job_details = GetJobDetails(job_id); if (!job_details || job_details->job_state == JobDetails::State::kDownloadsComplete || job_details->job_state == JobDetails::State::kJobComplete) { // The cancel event arrived after the fetch was complete; ignore it. return; } job_details->cancelled_from_ui = true; Abort(job_id); if (auto client = GetClient(job_id)) { // The |download_guid| is not releavnt here as the job has already // been aborted and is assumed to have been removed. client->OnJobCancelled( job_id, "" /* download_guid */, blink::mojom::BackgroundFetchFailureReason::CANCELLED_FROM_UI); } } void BackgroundFetchDelegateBase::OnUiFinished(const std::string& job_id) { DCHECK_CURRENTLY_ON(content::BrowserThread::UI); job_details_map_.erase(job_id); DoCleanUpUi(job_id); } void BackgroundFetchDelegateBase::OnUiActivated(const std::string& job_id) { DCHECK_CURRENTLY_ON(content::BrowserThread::UI); if (auto client = GetClient(job_id)) client->OnUIActivated(job_id); } JobDetails* BackgroundFetchDelegateBase::GetJobDetails( const std::string& job_id, bool allow_null) { DCHECK_CURRENTLY_ON(content::BrowserThread::UI); auto job_details_iter = job_details_map_.find(job_id); if (job_details_iter == job_details_map_.end()) { if (!allow_null) NOTREACHED(); return nullptr; } return &job_details_iter->second; } void BackgroundFetchDelegateBase::StartDownload(const std::string& job_id, download::DownloadParams params, bool has_request_body) { DCHECK_CURRENTLY_ON(content::BrowserThread::UI); GetJobDetails(job_id)->current_fetch_guids.emplace(params.guid, has_request_body); GetDownloadService()->StartDownload(std::move(params)); } void BackgroundFetchDelegateBase::Abort(const std::string& job_id) { DCHECK_CURRENTLY_ON(content::BrowserThread::UI); JobDetails* job_details = GetJobDetails(job_id, /*allow_null=*/true); if (!job_details) return; job_details->job_state = JobDetails::State::kCancelled; for (const auto& download_guid_pair : job_details->current_fetch_guids) { GetDownloadService()->CancelDownload(download_guid_pair.first); download_job_id_map_.erase(download_guid_pair.first); } DoUpdateUi(job_id); } void BackgroundFetchDelegateBase::MarkJobComplete(const std::string& job_id) { JobDetails* job_details = GetJobDetails(job_id); if (job_details->job_state == JobDetails::State::kCancelled) { OnUiFinished(job_id); return; } job_details->job_state = JobDetails::State::kJobComplete; // Clear the |job_details| internals that are no longer needed. job_details->current_fetch_guids.clear(); } void BackgroundFetchDelegateBase::FailFetch(const std::string& job_id, const std::string& download_guid) { // Save a copy before Abort() deletes the reference. const std::string unique_id = job_id; Abort(job_id); if (auto client = GetClient(unique_id)) { client->OnJobCancelled( download_guid, unique_id, blink::mojom::BackgroundFetchFailureReason::DOWNLOAD_TOTAL_EXCEEDED); } } void BackgroundFetchDelegateBase::OnDownloadStarted( const std::string& download_guid, std::unique_ptr response) { DCHECK_CURRENTLY_ON(content::BrowserThread::UI); auto download_job_id_iter = download_job_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_id_iter == download_job_id_map_.end()) return; const std::string& job_id = download_job_id_iter->second; JobDetails* job_details = GetJobDetails(job_id); if (job_details->client) { job_details->client->OnDownloadStarted(job_id, download_guid, std::move(response)); } // Update the upload progress. auto it = job_details->current_fetch_guids.find(download_guid); DCHECK(it != job_details->current_fetch_guids.end()); job_details->fetch_description->uploaded_bytes += it->second.body_size_bytes; } void BackgroundFetchDelegateBase::OnDownloadUpdated( const std::string& download_guid, uint64_t bytes_uploaded, uint64_t bytes_downloaded) { DCHECK_CURRENTLY_ON(content::BrowserThread::UI); auto download_job_id_iter = download_job_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_id_iter == download_job_id_map_.end()) return; const std::string job_id = download_job_id_iter->second; JobDetails* job_details = GetJobDetails(job_id); job_details->UpdateInProgressBytes(download_guid, bytes_uploaded, bytes_downloaded); if (job_details->fetch_description->download_total_bytes && job_details->fetch_description->download_total_bytes < job_details->GetDownloadedBytes()) { // Fail the fetch if total download size was set too low. // We only do this if total download size is specified. If not specified, // this check is skipped. This is to allow for situations when the // total download size cannot be known when invoking fetch. FailFetch(job_id, download_guid); return; } DoUpdateUi(job_id); if (job_details->client) { job_details->client->OnDownloadUpdated(job_id, download_guid, bytes_uploaded, bytes_downloaded); } } void BackgroundFetchDelegateBase::OnDownloadFailed( const std::string& download_guid, std::unique_ptr result) { DCHECK_CURRENTLY_ON(content::BrowserThread::UI); auto download_job_id_iter = download_job_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_id_iter == download_job_id_map_.end()) return; const std::string& job_id = download_job_id_iter->second; JobDetails* job_details = GetJobDetails(job_id); job_details->UpdateJobOnDownloadComplete(download_guid); DoUpdateUi(job_id); // The client cancelled or aborted the download so no need to notify it. if (result->failure_reason == content::BackgroundFetchResult::FailureReason::CANCELLED) { return; } if (job_details->client) { job_details->client->OnDownloadComplete(job_id, download_guid, std::move(result)); } download_job_id_map_.erase(download_guid); } void BackgroundFetchDelegateBase::OnDownloadSucceeded( const std::string& download_guid, std::unique_ptr result) { DCHECK_CURRENTLY_ON(content::BrowserThread::UI); auto download_job_id_iter = download_job_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_id_iter == download_job_id_map_.end()) return; const std::string& job_id = download_job_id_iter->second; JobDetails* job_details = GetJobDetails(job_id); job_details->UpdateJobOnDownloadComplete(download_guid); job_details->fetch_description->downloaded_bytes += context_->IsOffTheRecord() ? result->blob_handle->size() : result->file_size; DoUpdateUi(job_id); if (job_details->client) { job_details->client->OnDownloadComplete(job_id, download_guid, std::move(result)); } download_job_id_map_.erase(download_guid); } void BackgroundFetchDelegateBase::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::UNEXPECTED_GUID: // The download started in a previous session. 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(); break; 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(); break; case StartResult::COUNT: NOTREACHED(); break; } } bool BackgroundFetchDelegateBase::IsGuidOutstanding( const std::string& guid) const { auto job_id_iter = download_job_id_map_.find(guid); if (job_id_iter == download_job_id_map_.end()) return false; auto job_details_iter = job_details_map_.find(job_id_iter->second); if (job_details_iter == job_details_map_.end()) return false; const std::vector& outstanding_guids = job_details_iter->second.fetch_description->outstanding_guids; return std::find(outstanding_guids.begin(), outstanding_guids.end(), guid) != outstanding_guids.end(); } void BackgroundFetchDelegateBase::RestartPausedDownload( const std::string& download_guid) { auto job_it = download_job_id_map_.find(download_guid); if (job_it == download_job_id_map_.end()) return; const std::string& job_id = job_it->second; GetJobDetails(job_id)->job_state = JobDetails::State::kStartedButPaused; DoUpdateUi(job_id); } std::set BackgroundFetchDelegateBase::TakeOutstandingGuids() { std::set outstanding_guids; for (auto& job_id_details : job_details_map_) { auto& job_details = job_id_details.second; // If the job is loaded at this point, then it already started // in a previous session. job_details.MarkJobAsStarted(); std::vector& job_outstanding_guids = job_details.fetch_description->outstanding_guids; for (std::string& outstanding_guid : job_outstanding_guids) outstanding_guids.insert(std::move(outstanding_guid)); job_outstanding_guids.clear(); } return outstanding_guids; } void BackgroundFetchDelegateBase::GetUploadData( const std::string& download_guid, download::GetUploadDataCallback callback) { auto job_it = download_job_id_map_.find(download_guid); // TODO(crbug.com/779012): When DownloadService fixes cancelled jobs calling // client methods, then this can be a DCHECK. if (job_it == download_job_id_map_.end()) { base::SequencedTaskRunnerHandle::Get()->PostTask( FROM_HERE, base::BindOnce(std::move(callback), /* request_body= */ nullptr)); return; } const std::string& job_id = job_it->second; JobDetails* job_details = GetJobDetails(job_id); if (job_details->current_fetch_guids.at(download_guid).status == JobDetails::RequestData::Status::kAbsent) { base::SequencedTaskRunnerHandle::Get()->PostTask( FROM_HERE, base::BindOnce(std::move(callback), /* request_body= */ nullptr)); return; } if (job_details->client) { job_details->client->GetUploadData( job_id, download_guid, base::BindOnce(&BackgroundFetchDelegateBase::DidGetUploadData, weak_ptr_factory_.GetWeakPtr(), job_id, download_guid, std::move(callback))); } } void BackgroundFetchDelegateBase::DidGetUploadData( const std::string& job_id, const std::string& download_guid, download::GetUploadDataCallback callback, blink::mojom::SerializedBlobPtr blob) { if (!blob || blob->uuid.empty()) { std::move(callback).Run(/* request_body= */ nullptr); return; } JobDetails* job_details = GetJobDetails(job_id, /*allow_null=*/true); if (!job_details) { std::move(callback).Run(/* request_body= */ nullptr); return; } DCHECK(job_details->current_fetch_guids.count(download_guid)); auto& request_data = job_details->current_fetch_guids.at(download_guid); request_data.body_size_bytes = blob->size; // Use a Data Pipe to transfer the blob. mojo::PendingRemote data_pipe_getter_remote; mojo::Remote blob_remote(std::move(blob->blob)); blob_remote->AsDataPipeGetter( data_pipe_getter_remote.InitWithNewPipeAndPassReceiver()); auto request_body = base::MakeRefCounted(); request_body->AppendDataPipe(std::move(data_pipe_getter_remote)); std::move(callback).Run(request_body); } base::WeakPtr BackgroundFetchDelegateBase::GetClient(const std::string& job_id) { auto it = job_details_map_.find(job_id); if (it == job_details_map_.end()) return nullptr; return it->second.client; } } // namespace background_fetch