blob: 1a298b9beb4adadebd784503abd9b7beff4bda9d [file] [log] [blame]
// Copyright 2017 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/background_fetch/background_fetch_delegate_impl.h"
#include <utility>
#include "base/check_op.h"
#include "base/containers/contains.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/notreached.h"
#include "base/strings/string_util.h"
#include "base/task/single_thread_task_runner.h"
#include "build/build_config.h"
#include "chrome/browser/download/background_download_service_factory.h"
#include "chrome/browser/metrics/ukm_background_recorder_service.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/background_fetch/job_details.h"
#include "components/download/public/background_service/background_download_service.h"
#include "components/download/public/common/download_features.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_description.h"
#include "content/public/browser/browser_thread.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "url/origin.h"
namespace {
constexpr char kBackgroundFetchNamespacePrefix[] = "background_fetch";
} // namespace
BackgroundFetchDelegateImpl::BackgroundFetchDelegateImpl(Profile* profile)
: background_fetch::BackgroundFetchDelegateBase(profile),
profile_(profile),
provider_namespace_(
offline_items_collection::OfflineContentAggregator::
CreateUniqueNameSpace(kBackgroundFetchNamespacePrefix,
profile->IsOffTheRecord())),
offline_content_aggregator_(OfflineContentAggregatorFactory::GetForKey(
profile->GetProfileKey())) {
DCHECK(profile_);
DCHECK(!provider_namespace_.empty());
offline_content_aggregator_->RegisterProvider(provider_namespace_, this);
// Ensure that downloads UI components are initialized to handle the UI
// updates.
if (!base::FeatureList::IsEnabled(
download::features::
kUseInProgressDownloadManagerForDownloadService)) {
profile_->GetDownloadManager();
}
}
BackgroundFetchDelegateImpl::~BackgroundFetchDelegateImpl() {
offline_content_aggregator_->UnregisterProvider(provider_namespace_);
}
void BackgroundFetchDelegateImpl::MarkJobComplete(const std::string& job_id) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
background_fetch::JobDetails* job_details = GetJobDetails(job_id);
RecordBackgroundFetchDeletingRegistrationUkmEvent(
job_details->fetch_description->origin, job_details->cancelled_from_ui);
BackgroundFetchDelegateBase::MarkJobComplete(job_id);
}
void BackgroundFetchDelegateImpl::UpdateUI(
const std::string& job_id,
const absl::optional<std::string>& title,
const absl::optional<SkBitmap>& icon) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(title || icon); // One of the UI options must be updatable.
DCHECK(!icon || !icon->isNull()); // The |icon|, if provided, is not null.
background_fetch::JobDetails* job_details =
GetJobDetails(job_id, /*allow_null=*/true);
if (!job_details)
return;
// Update the title, if it's different.
if (title && job_details->fetch_description->title != *title)
job_details->fetch_description->title = *title;
DCHECK(base::Contains(ui_state_map_, job_id));
UiState& ui_state = ui_state_map_[job_id];
if (icon) {
job_details->fetch_description->icon = *icon;
offline_items_collection::UpdateDelta update_delta;
update_delta.visuals_changed = true;
ui_state.update_delta = update_delta;
}
bool should_update_visuals = ui_state.update_delta.has_value()
? ui_state.update_delta->visuals_changed
: false;
#if !BUILDFLAG(IS_ANDROID)
should_update_visuals = false;
#endif
if (!should_update_visuals) {
// Notify the client that the UI updates have been handed over.
if (job_details->client)
job_details->client->OnUIUpdated(job_id);
}
DoUpdateUi(job_id);
}
void BackgroundFetchDelegateImpl::OpenItem(
const offline_items_collection::OpenParams& open_params,
const offline_items_collection::ContentId& id) {
OnUiActivated(id.id);
auto* job_details = GetJobDetails(id.id, /*allow_null=*/true);
if (job_details && job_details->IsComplete())
OnUiFinished(id.id);
}
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) {
BackgroundFetchDelegateBase::CancelDownload(id.id);
}
void BackgroundFetchDelegateImpl::PauseDownload(
const offline_items_collection::ContentId& id) {
UpdateOfflineItem(id.id);
BackgroundFetchDelegateBase::PauseDownload(id.id);
}
void BackgroundFetchDelegateImpl::ResumeDownload(
const offline_items_collection::ContentId& id,
bool has_user_gesture) {
UpdateOfflineItem(id.id);
BackgroundFetchDelegateBase::ResumeDownload(id.id);
}
void BackgroundFetchDelegateImpl::GetItemById(
const offline_items_collection::ContentId& id,
SingleItemCallback callback) {
auto iter = ui_state_map_.find(id.id);
absl::optional<offline_items_collection::OfflineItem> offline_item;
if (iter != ui_state_map_.end())
offline_item = iter->second.offline_item;
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), offline_item));
}
void BackgroundFetchDelegateImpl::GetAllItems(MultipleItemCallback callback) {
OfflineItemList item_list;
for (auto& entry : ui_state_map_)
item_list.push_back(entry.second.offline_item);
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), item_list));
}
void BackgroundFetchDelegateImpl::GetVisualsForItem(
const offline_items_collection::ContentId& id,
GetVisualsOptions options,
VisualsCallback callback) {
if (!options.get_icon) {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), id, nullptr));
return;
}
// 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.
auto visuals =
std::make_unique<offline_items_collection::OfflineItemVisuals>();
background_fetch::JobDetails* job_details =
GetJobDetails(id.id, /*allow_null=*/true);
if (job_details) {
visuals->icon =
gfx::Image::CreateFrom1xBitmap(job_details->fetch_description->icon);
if (job_details->client &&
job_details->job_state ==
background_fetch::JobDetails::State::kDownloadsComplete) {
job_details->client->OnUIUpdated(id.id);
}
}
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), id, std::move(visuals)));
}
void BackgroundFetchDelegateImpl::GetShareInfoForItem(
const offline_items_collection::ContentId& id,
ShareCallback callback) {
// TODO(xingliu): Provide OfflineItemShareInfo to |callback|.
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), id,
nullptr /* OfflineItemShareInfo */));
}
void BackgroundFetchDelegateImpl::RenameItem(
const offline_items_collection::ContentId& id,
const std::string& name,
RenameCallback callback) {
NOTIMPLEMENTED();
}
download::BackgroundDownloadService*
BackgroundFetchDelegateImpl::GetDownloadService() {
return BackgroundDownloadServiceFactory::GetInstance()->GetForKey(
profile_->GetProfileKey());
}
void BackgroundFetchDelegateImpl::OnJobDetailsCreated(
const std::string& job_id) {
DCHECK(!base::Contains(ui_state_map_, job_id));
UiState& ui_state = ui_state_map_[job_id];
offline_items_collection::OfflineItem offline_item(
offline_items_collection::ContentId(provider_namespace_, job_id));
offline_item.creation_time = base::Time::Now();
offline_item.is_off_the_record = profile_->IsOffTheRecord();
#if BUILDFLAG(IS_ANDROID)
if (profile_->IsOffTheRecord())
offline_item.otr_profile_id = profile_->GetOTRProfileID().Serialize();
#endif
offline_item.original_url =
GetJobDetails(job_id)->fetch_description->origin.GetURL();
ui_state.offline_item = offline_item;
UpdateOfflineItem(job_id);
}
void BackgroundFetchDelegateImpl::DoShowUi(const std::string& job_id) {
NotifyItemsAdded({ui_state_map_[job_id].offline_item});
}
// 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::DoUpdateUi(const std::string& job_id) {
// Update the OfflineItem that controls the contents of download
// notifications and notify any OfflineContentProvider::Observer that was
// registered with this instance.
UpdateOfflineItem(job_id);
if (ui_state_map_.find(job_id) == ui_state_map_.end()) {
// This is a delayed update event. The Background Fetch has already
// completed.
return;
}
UiState& ui_state = ui_state_map_[job_id];
auto update_delta = std::move(ui_state.update_delta);
NotifyItemUpdated(ui_state.offline_item, update_delta);
}
void BackgroundFetchDelegateImpl::DoCleanUpUi(const std::string& job_id) {
ui_state_map_.erase(job_id);
// Note that the entry in `ui_state_map_` will leak if OnUiFinished is never
// called, and it's not called when the notification is dismissed without
// being clicked. See crbug.com/1190390
}
void BackgroundFetchDelegateImpl::UpdateOfflineItem(const std::string& job_id) {
background_fetch::JobDetails* job_details =
GetJobDetails(job_id, /*allow_null=*/true);
if (!job_details)
return;
content::BackgroundFetchDescription* fetch_description =
job_details->fetch_description.get();
DCHECK_GT(fetch_description->total_requests, 0);
offline_items_collection::OfflineItem* offline_item =
&ui_state_map_[job_id].offline_item;
using JobState = background_fetch::JobDetails::State;
if (job_details->ShouldReportProgressBySize()) {
offline_item->progress.value = job_details->GetProcessedBytes();
// If we have completed all downloads, update progress max to the processed
// bytes in case the provided totals were set too high. This avoids
// unnecessary jumping in the progress bar.
uint64_t completed_bytes =
fetch_description->downloaded_bytes + fetch_description->uploaded_bytes;
uint64_t total_bytes = fetch_description->download_total_bytes +
fetch_description->upload_total_bytes;
offline_item->progress.max =
job_details->job_state == JobState::kDownloadsComplete ? completed_bytes
: total_bytes;
} else {
offline_item->progress.value = fetch_description->completed_requests;
offline_item->progress.max = fetch_description->total_requests;
}
offline_item->progress.unit =
offline_items_collection::OfflineItemProgressUnit::PERCENTAGE;
offline_item->title = fetch_description->title;
offline_item->promote_origin = true;
offline_item->is_transient = true;
offline_item->is_resumable = true;
using OfflineItemState = offline_items_collection::OfflineItemState;
switch (job_details->job_state) {
case JobState::kCancelled:
offline_item->state = OfflineItemState::CANCELLED;
break;
case JobState::kDownloadsComplete:
// This includes cases when the download failed, or completed but the
// response was an HTTP error, e.g. 404.
offline_item->state = OfflineItemState::COMPLETE;
offline_item->is_openable = true;
break;
case JobState::kPendingWillStartPaused:
case JobState::kStartedButPaused:
offline_item->state = OfflineItemState::PAUSED;
break;
case JobState::kJobComplete:
// There shouldn't be any updates at this point.
NOTREACHED();
break;
default:
offline_item->state = OfflineItemState::IN_PROGRESS;
}
}
void BackgroundFetchDelegateImpl::
RecordBackgroundFetchDeletingRegistrationUkmEvent(
const url::Origin& origin,
bool user_initiated_abort) {
auto* ukm_background_service =
ukm::UkmBackgroundRecorderFactory::GetForProfile(profile_);
ukm_background_service->GetBackgroundSourceIdIfAllowed(
origin,
base::BindOnce(&BackgroundFetchDelegateImpl::DidGetBackgroundSourceId,
weak_ptr_factory_.GetWeakPtr(), user_initiated_abort));
}
void BackgroundFetchDelegateImpl::DidGetBackgroundSourceId(
bool user_initiated_abort,
absl::optional<ukm::SourceId> source_id) {
// This background event did not meet the requirements for the UKM service.
if (!source_id)
return;
ukm::builders::BackgroundFetchDeletingRegistration(*source_id)
.SetUserInitiatedAbort(user_initiated_abort)
.Record(ukm::UkmRecorder::Get());
}
BackgroundFetchDelegateImpl::UiState::UiState() = default;
BackgroundFetchDelegateImpl::UiState::~UiState() = default;