// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/download/internal/background_service/ios/background_download_service_impl.h"
#include <utility>
#include <vector>
#include "base/logging.h"
#include "base/notreached.h"
#include "base/numerics/safe_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "components/download/internal/background_service/client_set.h"
#include "components/download/internal/background_service/config.h"
#include "components/download/internal/background_service/entry.h"
#include "components/download/internal/background_service/file_monitor.h"
#include "components/download/internal/background_service/ios/background_download_task_helper.h"
#include "components/download/internal/background_service/ios/entry_utils.h"
#include "components/download/internal/background_service/log_sink.h"
#include "components/download/internal/background_service/stats.h"
#include "components/download/public/background_service/client.h"
#include "components/download/public/background_service/download_metadata.h"
#include "components/download/public/background_service/download_params.h"
#include "components/download/public/background_service/logger.h"
namespace download {
namespace {
// Interval to throttle the download update that results in a database update.
const base::TimeDelta kUpdateInterval = base::Seconds(5);
} // namespace
BackgroundDownloadServiceImpl::BackgroundDownloadServiceImpl(
std::unique_ptr<ClientSet> clients,
std::unique_ptr<Model> model,
std::unique_ptr<BackgroundDownloadTaskHelper> download_helper,
std::unique_ptr<FileMonitor> file_monitor,
const base::FilePath& download_dir,
std::unique_ptr<Logger> logger,
LogSink* log_sink,
base::Clock* clock)
: config_(std::make_unique<Configuration>()),
service_config_(config_.get()),
clients_(std::move(clients)),
model_(std::move(model)),
download_helper_(std::move(download_helper)),
file_monitor_(std::move(file_monitor)),
logger_(std::move(logger)),
log_sink_(log_sink),
clock_(clock),
download_dir_(download_dir) {
// iOS doesn't use driver interface, mark it ready.
startup_status_.driver_ok = true;
}
BackgroundDownloadServiceImpl::~BackgroundDownloadServiceImpl() = default;
void BackgroundDownloadServiceImpl::Initialize(base::OnceClosure callback) {
init_callback_ = std::move(callback);
model_->Initialize(this);
}
const ServiceConfig& BackgroundDownloadServiceImpl::GetConfig() {
NOTREACHED_IN_MIGRATION() << " This function is not supported on iOS.";
return service_config_;
}
void BackgroundDownloadServiceImpl::OnStartScheduledTask(
DownloadTaskType task_type,
TaskFinishedCallback callback) {
NOTREACHED_IN_MIGRATION() << " This function is not supported on iOS.";
}
bool BackgroundDownloadServiceImpl::OnStopScheduledTask(
DownloadTaskType task_type) {
NOTREACHED_IN_MIGRATION() << " This function is not supported on iOS.";
return true;
}
BackgroundDownloadService::ServiceStatus
BackgroundDownloadServiceImpl::GetStatus() {
if (startup_status_.Failed())
return BackgroundDownloadService::ServiceStatus::UNAVAILABLE;
return startup_status_.Complete()
? BackgroundDownloadService::ServiceStatus::READY
: BackgroundDownloadService::ServiceStatus::STARTING_UP;
}
void BackgroundDownloadServiceImpl::StartDownload(
DownloadParams download_params) {
if (GetStatus() != BackgroundDownloadService::ServiceStatus::READY) {
LOG(ERROR) << "Background download service is not intialized successfully.";
InvokeStartCallback(download_params.client, download_params.guid,
DownloadParams::StartResult::INTERNAL_ERROR,
std::move(download_params.callback));
return;
}
if (start_callbacks_.find(download_params.guid) != start_callbacks_.end() ||
model_->Get(download_params.guid) != nullptr) {
InvokeStartCallback(download_params.client, download_params.guid,
DownloadParams::StartResult::UNEXPECTED_GUID,
std::move(download_params.callback));
return;
}
DCHECK(!download_params.guid.empty());
start_callbacks_.emplace(download_params.guid,
std::move(download_params.callback));
Entry entry(download_params);
entry.target_file_path = download_dir_.AppendASCII(download_params.guid);
entry.create_time = clock_->Now();
entry.state = Entry::State::ACTIVE;
entry.custom_data = std::move(download_params.custom_data);
model_->Add(entry);
}
void BackgroundDownloadServiceImpl::PauseDownload(const std::string& guid) {
NOTREACHED_IN_MIGRATION() << " This function is not supported on iOS.";
}
void BackgroundDownloadServiceImpl::ResumeDownload(const std::string& guid) {
NOTREACHED_IN_MIGRATION() << " This function is not supported on iOS.";
}
void BackgroundDownloadServiceImpl::CancelDownload(const std::string& guid) {
cancelled_downloads_.emplace(guid);
}
void BackgroundDownloadServiceImpl::ChangeDownloadCriteria(
const std::string& guid,
const SchedulingParams& params) {
NOTREACHED_IN_MIGRATION() << " This function is not supported on iOS.";
}
Logger* BackgroundDownloadServiceImpl::GetLogger() {
return logger_.get();
}
void BackgroundDownloadServiceImpl::HandleEventsForBackgroundURLSession(
base::OnceClosure completion_handler) {
download_helper_->HandleEventsForBackgroundURLSession(
std::move(completion_handler));
}
void BackgroundDownloadServiceImpl::OnModelReady(bool success) {
startup_status_.model_ok = success;
if (!success) {
DCHECK(startup_status_.Failed());
stats::LogStartUpResult(false, stats::StartUpResult::FAILURE_REASON_MODEL);
NotifyServiceUnavailable();
return;
}
PruneDbRecords();
file_monitor_->Initialize(
base::BindOnce(&BackgroundDownloadServiceImpl::OnFileMonitorInitialized,
weak_ptr_factory_.GetWeakPtr()));
}
void BackgroundDownloadServiceImpl::PruneDbRecords() {
// Clean up expired entries or entries without a client, disregard whether
// they are completed.
std::set<std::string> entries_to_remove;
for (Entry* entry : model_->PeekEntries()) {
download::Client* client = clients_->GetClient(entry->client);
// TODO(xingliu): Ask client whether we can delete the file?
if (!client ||
clock_->Now() - entry->create_time > config_->file_keep_alive_time) {
entries_to_remove.insert(entry->guid);
}
// On iOS, we don't implement any resumption mechanism, so unfinished
// downloads should be deleted.
if (entry->state != Entry::State::COMPLETE) {
entries_to_remove.insert(entry->guid);
}
}
for (const auto& guid : entries_to_remove)
model_->Remove(guid);
}
void BackgroundDownloadServiceImpl::OnFileMonitorInitialized(bool success) {
if (!success) {
startup_status_.file_monitor_ok = false;
DCHECK(startup_status_.Complete());
DCHECK(startup_status_.Failed());
stats::LogStartUpResult(false,
stats::StartUpResult::FAILURE_REASON_FILE_MONITOR);
NotifyServiceUnavailable();
return;
}
// Clean up the download file directory on a background thread.
file_monitor_->DeleteUnknownFiles(
model_->PeekEntries(), {},
base::BindOnce(&BackgroundDownloadServiceImpl::OnFilesPruned,
weak_ptr_factory_.GetWeakPtr()));
}
void BackgroundDownloadServiceImpl::OnFilesPruned() {
// Initialization is done.
startup_status_.file_monitor_ok = true;
DCHECK(startup_status_.Ok());
log_sink_->OnServiceStatusChanged();
stats::LogStartUpResult(false, stats::StartUpResult::SUCCESS);
if (init_callback_)
std::move(init_callback_).Run();
// Report download metadata to clients.
auto metadata_map = util::MapEntriesToMetadataForClients(
clients_->GetRegisteredClients(), model_->PeekEntries());
for (DownloadClient client_id : clients_->GetRegisteredClients()) {
clients_->GetClient(client_id)->OnServiceInitialized(
/*state_lost=*/false, metadata_map[client_id]);
}
log_sink_->OnServiceDownloadsAvailable();
}
void BackgroundDownloadServiceImpl::NotifyServiceUnavailable() {
for (DownloadClient client_id : clients_->GetRegisteredClients())
clients_->GetClient(client_id)->OnServiceUnavailable();
log_sink_->OnServiceStatusChanged();
}
void BackgroundDownloadServiceImpl::InvokeStartCallback(
DownloadClient client,
const std::string& guid,
DownloadParams::StartResult result,
DownloadParams::StartCallback callback) {
log_sink_->OnServiceRequestMade(client, guid, result);
stats::LogStartDownloadResult(client, result);
if (callback) {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), guid, result));
}
}
void BackgroundDownloadServiceImpl::OnModelHardRecoverComplete(bool success) {}
void BackgroundDownloadServiceImpl::OnItemAdded(bool success,
DownloadClient client,
const std::string& guid) {
DownloadParams::StartCallback callback = std::move(start_callbacks_[guid]);
start_callbacks_.erase(guid);
if (!success) {
InvokeStartCallback(client, guid,
DownloadParams::StartResult::INTERNAL_ERROR,
std::move(callback));
return;
}
Entry* entry = model_->Get(guid);
DCHECK(entry);
InvokeStartCallback(client, guid, DownloadParams::StartResult::ACCEPTED,
std::move(callback));
download_helper_->StartDownload(
entry->guid, entry->target_file_path, entry->request_params,
entry->scheduling_params,
base::BindOnce(&BackgroundDownloadServiceImpl::OnDownloadFinished,
weak_ptr_factory_.GetWeakPtr(), entry->client,
entry->guid),
base::BindRepeating(&BackgroundDownloadServiceImpl::OnDownloadUpdated,
weak_ptr_factory_.GetWeakPtr(), entry->client,
entry->guid));
}
void BackgroundDownloadServiceImpl::OnItemUpdated(bool success,
DownloadClient client,
const std::string& guid) {}
void BackgroundDownloadServiceImpl::OnItemRemoved(bool success,
DownloadClient client,
const std::string& guid) {}
Controller::State BackgroundDownloadServiceImpl::GetControllerState() {
switch (GetStatus()) {
case ServiceStatus::STARTING_UP:
return Controller::State::INITIALIZING;
case ServiceStatus::READY:
return Controller::State::READY;
case ServiceStatus::UNAVAILABLE:
return Controller::State::UNAVAILABLE;
}
}
const StartupStatus& BackgroundDownloadServiceImpl::GetStartupStatus() {
return startup_status_;
}
LogSource::EntryDetailsList
BackgroundDownloadServiceImpl::GetServiceDownloads() {
EntryDetailsList list;
auto entries = model_->PeekEntries();
for (download::Entry* entry : entries) {
list.push_back(std::make_pair(entry, std::nullopt));
}
return list;
}
std::optional<LogSource::EntryDetails>
BackgroundDownloadServiceImpl::GetServiceDownload(const std::string& guid) {
auto* entry = model_->Get(guid);
return std::optional<LogSource::EntryDetails>(
std::make_pair(entry, std::nullopt));
}
void BackgroundDownloadServiceImpl::OnDownloadFinished(
DownloadClient download_client,
const std::string& guid,
bool success,
const base::FilePath& file_path,
int64_t file_size) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (cancelled_downloads_.find(guid) != cancelled_downloads_.end()) {
cancelled_downloads_.erase(guid);
return;
}
download::Client* client = clients_->GetClient(download_client);
if (!client)
return;
// TODO(xingliu): Plumb more details from platform api for failure reasons and
// bytes downloaded.
Entry* entry = model_->Get(guid);
if (!success) {
stats::LogDownloadCompletion(download_client, CompletionType::FAIL,
file_size);
if (entry) {
log_sink_->OnServiceDownloadFailed(CompletionType::UNKNOWN, *entry);
model_->Remove(guid);
}
client->OnDownloadFailed(guid, CompletionInfo(),
download::Client::FailureReason::UNKNOWN);
return;
}
if (!entry)
return;
entry->bytes_downloaded = base::saturated_cast<uint64_t>(file_size);
entry->completion_time = clock_->Now();
entry->state = Entry::State::COMPLETE;
model_->Update(*entry);
log_sink_->OnServiceDownloadChanged(guid);
stats::LogDownloadCompletion(download_client, CompletionType::SUCCEED,
file_size);
CompletionInfo completion_info;
completion_info.path = file_path;
completion_info.custom_data = entry->custom_data;
client->OnDownloadSucceeded(guid, completion_info);
}
void BackgroundDownloadServiceImpl::OnDownloadUpdated(
DownloadClient download_client,
const std::string& guid,
int64_t bytes_downloaded) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (cancelled_downloads_.find(guid) != cancelled_downloads_.end()) {
return;
}
uint64_t bytes_count = base::saturated_cast<uint64_t>(bytes_downloaded);
MaybeUpdateProgress(guid, bytes_count);
log_sink_->OnServiceDownloadChanged(guid);
download::Client* client = clients_->GetClient(download_client);
if (!client)
return;
client->OnDownloadUpdated(guid, /*bytes_uploaded*/ 0u, bytes_count);
}
void BackgroundDownloadServiceImpl::MaybeUpdateProgress(
const std::string& guid,
uint64_t bytes_downloaded) {
// Throttle the model update frequency.
if (clock_->Now() - update_time_ < kUpdateInterval)
return;
update_time_ = clock_->Now();
Entry* entry = model_->Get(guid);
DCHECK_GE(bytes_downloaded, 0u);
entry->bytes_downloaded = bytes_downloaded;
model_->Update(*entry);
}
} // namespace download