| // 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 <utility> |
| |
| #include "base/bind.h" |
| #include "base/callback_helpers.h" |
| #include "base/check_op.h" |
| #include "base/notreached.h" |
| #include "base/task/post_task.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 defined(OS_ANDROID) |
| display_size = gfx::Size(192, 192); |
| #endif |
| std::move(callback).Run(display_size); |
| } |
| |
| void BackgroundFetchDelegateBase::CreateDownloadJob( |
| base::WeakPtr<Client> client, |
| std::unique_ptr<content::BackgroundFetchDescription> 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); |
| |
| 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)) { |
| client->OnJobCancelled( |
| job_id, blink::mojom::BackgroundFetchFailureReason::CANCELLED_FROM_UI); |
| } |
| } |
| |
| void BackgroundFetchDelegateBase::OnUiFinished(const std::string& job_id, |
| bool activated) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| if (activated) { |
| if (auto client = GetClient(job_id)) |
| client->OnUIActivated(job_id); |
| } |
| |
| job_details_map_.erase(job_id); |
| DoCleanUpUi(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, /*activated=*/false); |
| 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) { |
| // 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( |
| unique_id, |
| blink::mojom::BackgroundFetchFailureReason::DOWNLOAD_TOTAL_EXCEEDED); |
| } |
| } |
| |
| void BackgroundFetchDelegateBase::OnDownloadStarted( |
| const std::string& download_guid, |
| std::unique_ptr<content::BackgroundFetchResponse> 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); |
| 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<content::BackgroundFetchResult> 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<content::BackgroundFetchResult> 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<std::string>& 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<std::string> BackgroundFetchDelegateBase::TakeOutstandingGuids() { |
| std::set<std::string> 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<std::string>& 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<network::mojom::DataPipeGetter> data_pipe_getter_remote; |
| mojo::Remote<blink::mojom::Blob> blob_remote(std::move(blob->blob)); |
| blob_remote->AsDataPipeGetter( |
| data_pipe_getter_remote.InitWithNewPipeAndPassReceiver()); |
| auto request_body = base::MakeRefCounted<network::ResourceRequestBody>(); |
| request_body->AppendDataPipe(std::move(data_pipe_getter_remote)); |
| |
| std::move(callback).Run(request_body); |
| } |
| |
| base::WeakPtr<content::BackgroundFetchDelegate::Client> |
| 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 |