blob: 38ef1a7f13d541137c0daa936b092f77a729cb92 [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 "components/download/internal/controller_impl.h"
#include <string>
#include <vector>
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/memory/ptr_util.h"
#include "base/threading/thread_task_runner_handle.h"
#include "components/download/internal/client_set.h"
#include "components/download/internal/config.h"
#include "components/download/internal/entry.h"
#include "components/download/internal/entry_utils.h"
#include "components/download/internal/file_monitor.h"
#include "components/download/internal/model.h"
#include "components/download/internal/scheduler/scheduler.h"
#include "components/download/internal/stats.h"
#include "components/download/public/client.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
namespace download {
namespace {
// Helper function to transit the state of |entry| to |new_state|.
void TransitTo(Entry* entry, Entry::State new_state, Model* model) {
DCHECK(entry);
if (entry->state == new_state)
return;
entry->state = new_state;
model->Update(*entry);
}
// Helper function to move from a CompletionType to a Client::FailureReason.
Client::FailureReason FailureReasonFromCompletionType(CompletionType type) {
// SUCCEED does not map to a FailureReason.
DCHECK_NE(CompletionType::SUCCEED, type);
switch (type) {
case CompletionType::FAIL:
return Client::FailureReason::NETWORK;
case CompletionType::ABORT:
return Client::FailureReason::ABORTED;
case CompletionType::TIMEOUT:
return Client::FailureReason::TIMEDOUT;
case CompletionType::UNKNOWN:
return Client::FailureReason::UNKNOWN;
case CompletionType::CANCEL:
return Client::FailureReason::CANCELLED;
default:
NOTREACHED();
}
return Client::FailureReason::UNKNOWN;
}
// Helper function to determine if more downloads can be activated based on
// configuration.
bool CanActivateMoreDownloads(Configuration* config,
uint32_t active_count,
uint32_t paused_count) {
if (config->max_concurrent_downloads <= paused_count + active_count ||
config->max_running_downloads <= active_count) {
return false;
}
return true;
}
} // namespace
ControllerImpl::ControllerImpl(
Configuration* config,
std::unique_ptr<ClientSet> clients,
std::unique_ptr<DownloadDriver> driver,
std::unique_ptr<Model> model,
std::unique_ptr<DeviceStatusListener> device_status_listener,
std::unique_ptr<Scheduler> scheduler,
std::unique_ptr<TaskScheduler> task_scheduler,
std::unique_ptr<FileMonitor> file_monitor,
const base::FilePath& download_file_dir)
: config_(config),
download_file_dir_(download_file_dir),
clients_(std::move(clients)),
driver_(std::move(driver)),
model_(std::move(model)),
device_status_listener_(std::move(device_status_listener)),
scheduler_(std::move(scheduler)),
task_scheduler_(std::move(task_scheduler)),
file_monitor_(std::move(file_monitor)),
initializing_internals_(false),
weak_ptr_factory_(this) {}
ControllerImpl::~ControllerImpl() = default;
void ControllerImpl::Initialize(const base::Closure& callback) {
DCHECK(!startup_status_.Complete());
init_callback_ = callback;
initializing_internals_ = true;
driver_->Initialize(this);
model_->Initialize(this);
file_monitor_->Initialize(base::Bind(&ControllerImpl::OnFileMonitorReady,
weak_ptr_factory_.GetWeakPtr()));
}
const StartupStatus* ControllerImpl::GetStartupStatus() {
return &startup_status_;
}
void ControllerImpl::StartDownload(const DownloadParams& params) {
DCHECK(startup_status_.Complete());
if (!startup_status_.Ok()) {
HandleStartDownloadResponse(params.client, params.guid,
DownloadParams::StartResult::INTERNAL_ERROR,
params.callback);
return;
}
DCHECK_LE(base::Time::Now(), params.scheduling_params.cancel_time);
KillTimedOutDownloads();
// TODO(dtrainor): Check if there are any downloads we can cancel. We don't
// want to return a BACKOFF if we technically could time out a download to
// start this one.
if (start_callbacks_.find(params.guid) != start_callbacks_.end() ||
model_->Get(params.guid) != nullptr) {
HandleStartDownloadResponse(params.client, params.guid,
DownloadParams::StartResult::UNEXPECTED_GUID,
params.callback);
return;
}
auto* client = clients_->GetClient(params.client);
if (!client) {
HandleStartDownloadResponse(params.client, params.guid,
DownloadParams::StartResult::UNEXPECTED_CLIENT,
params.callback);
return;
}
uint32_t client_count =
util::GetNumberOfEntriesForClient(params.client, model_->PeekEntries());
if (client_count >= config_->max_scheduled_downloads) {
HandleStartDownloadResponse(params.client, params.guid,
DownloadParams::StartResult::BACKOFF,
params.callback);
return;
}
start_callbacks_[params.guid] = params.callback;
Entry entry(params);
entry.target_file_path = download_file_dir_.AppendASCII(params.guid);
model_->Add(entry);
}
void ControllerImpl::PauseDownload(const std::string& guid) {
DCHECK(startup_status_.Complete());
if (!startup_status_.Ok())
return;
auto* entry = model_->Get(guid);
if (!entry || entry->state == Entry::State::PAUSED ||
entry->state == Entry::State::COMPLETE ||
entry->state == Entry::State::NEW) {
return;
}
TransitTo(entry, Entry::State::PAUSED, model_.get());
UpdateDriverState(entry);
// Pausing a download may yield a concurrent slot to start a new download, and
// may change the scheduling criteria.
ActivateMoreDownloads();
}
void ControllerImpl::ResumeDownload(const std::string& guid) {
DCHECK(startup_status_.Complete());
if (!startup_status_.Ok())
return;
auto* entry = model_->Get(guid);
DCHECK(entry);
if (entry->state != Entry::State::PAUSED)
return;
TransitTo(entry, Entry::State::ACTIVE, model_.get());
UpdateDriverState(entry);
ActivateMoreDownloads();
}
void ControllerImpl::CancelDownload(const std::string& guid) {
DCHECK(startup_status_.Complete());
if (!startup_status_.Ok())
return;
auto* entry = model_->Get(guid);
if (!entry)
return;
if (entry->state == Entry::State::NEW) {
// Check if we're currently trying to add the download.
DCHECK(start_callbacks_.find(entry->guid) != start_callbacks_.end());
HandleStartDownloadResponse(entry->client, guid,
DownloadParams::StartResult::CLIENT_CANCELLED);
return;
}
HandleCompleteDownload(CompletionType::CANCEL, guid);
}
void ControllerImpl::ChangeDownloadCriteria(const std::string& guid,
const SchedulingParams& params) {
DCHECK(startup_status_.Complete());
if (!startup_status_.Ok())
return;
auto* entry = model_->Get(guid);
if (!entry || entry->scheduling_params == params) {
DVLOG(1) << "Try to update the same scheduling parameters.";
return;
}
UpdateDriverState(entry);
// Update the scheduling parameters.
entry->scheduling_params = params;
model_->Update(*entry);
ActivateMoreDownloads();
}
DownloadClient ControllerImpl::GetOwnerOfDownload(const std::string& guid) {
DCHECK(startup_status_.Complete());
if (!startup_status_.Ok())
return DownloadClient::INVALID;
auto* entry = model_->Get(guid);
return entry ? entry->client : DownloadClient::INVALID;
}
void ControllerImpl::OnStartScheduledTask(
DownloadTaskType task_type,
const TaskFinishedCallback& callback) {
DCHECK(startup_status_.Complete());
task_finished_callbacks_[task_type] = callback;
if (!startup_status_.Ok()) {
HandleTaskFinished(task_type, false,
stats::ScheduledTaskStatus::ABORTED_ON_FAILED_INIT);
return;
}
if (task_type == DownloadTaskType::DOWNLOAD_TASK) {
ActivateMoreDownloads();
} else if (task_type == DownloadTaskType::CLEANUP_TASK) {
RemoveCleanupEligibleDownloads();
}
}
bool ControllerImpl::OnStopScheduledTask(DownloadTaskType task_type) {
HandleTaskFinished(task_type, false,
stats::ScheduledTaskStatus::CANCELLED_ON_STOP);
return true;
}
void ControllerImpl::OnCompleteCleanupTask() {
HandleTaskFinished(DownloadTaskType::CLEANUP_TASK, false,
stats::ScheduledTaskStatus::COMPLETED_NORMALLY);
}
void ControllerImpl::RemoveCleanupEligibleDownloads() {
auto timed_out_entries = file_monitor_->CleanupFilesForCompletedEntries(
model_->PeekEntries(), base::Bind(&ControllerImpl::OnCompleteCleanupTask,
weak_ptr_factory_.GetWeakPtr()));
for (auto* entry : timed_out_entries) {
DCHECK_EQ(Entry::State::COMPLETE, entry->state);
model_->Remove(entry->guid);
}
}
void ControllerImpl::HandleTaskFinished(DownloadTaskType task_type,
bool needs_reschedule,
stats::ScheduledTaskStatus status) {
if (task_finished_callbacks_.find(task_type) ==
task_finished_callbacks_.end()) {
return;
}
if (status != stats::ScheduledTaskStatus::CANCELLED_ON_STOP) {
base::ResetAndReturn(&task_finished_callbacks_[task_type])
.Run(needs_reschedule);
}
// TODO(dtrainor): It might be useful to log how many downloads we have
// running when we're asked to stop processing.
stats::LogScheduledTaskStatus(task_type, status);
task_finished_callbacks_.erase(task_type);
}
void ControllerImpl::OnDriverReady(bool success) {
DCHECK(!startup_status_.driver_ok.has_value());
startup_status_.driver_ok = success;
AttemptToFinalizeSetup();
}
void ControllerImpl::OnDownloadCreated(const DriverEntry& download) {
if (initializing_internals_)
return;
Entry* entry = model_->Get(download.guid);
if (!entry) {
HandleExternalDownload(download.guid, true);
return;
}
download::Client* client = clients_->GetClient(entry->client);
DCHECK(client);
using ShouldDownload = download::Client::ShouldDownload;
ShouldDownload should_download = client->OnDownloadStarted(
download.guid, download.url_chain, download.response_headers);
if (should_download == ShouldDownload::ABORT) {
HandleCompleteDownload(CompletionType::ABORT, entry->guid);
}
}
void ControllerImpl::OnDownloadFailed(const DriverEntry& download,
FailureType failure_type) {
if (initializing_internals_)
return;
Entry* entry = model_->Get(download.guid);
if (!entry) {
HandleExternalDownload(download.guid, false);
return;
}
if (failure_type == FailureType::RECOVERABLE) {
UpdateDriverState(entry);
} else {
// TODO(dtrainor, xingliu): We probably have to prevent cancel calls from
// coming through here as we remove downloads (especially through
// initialization).
HandleCompleteDownload(CompletionType::FAIL, download.guid);
}
}
void ControllerImpl::OnDownloadSucceeded(const DriverEntry& download) {
if (initializing_internals_)
return;
Entry* entry = model_->Get(download.guid);
if (!entry) {
HandleExternalDownload(download.guid, false);
return;
}
HandleCompleteDownload(CompletionType::SUCCEED, download.guid);
}
void ControllerImpl::OnDownloadUpdated(const DriverEntry& download) {
if (initializing_internals_)
return;
Entry* entry = model_->Get(download.guid);
if (!entry) {
HandleExternalDownload(download.guid, !download.paused);
return;
}
DCHECK_EQ(download.state, DriverEntry::State::IN_PROGRESS);
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::Bind(&ControllerImpl::SendOnDownloadUpdated,
weak_ptr_factory_.GetWeakPtr(), entry->client,
download.guid, download.bytes_downloaded));
}
void ControllerImpl::OnFileMonitorReady(bool success) {
DCHECK(!startup_status_.file_monitor_ok.has_value());
startup_status_.file_monitor_ok = success;
AttemptToFinalizeSetup();
}
void ControllerImpl::OnModelReady(bool success) {
DCHECK(!startup_status_.model_ok.has_value());
startup_status_.model_ok = success;
AttemptToFinalizeSetup();
}
void ControllerImpl::OnItemAdded(bool success,
DownloadClient client,
const std::string& guid) {
// If the StartCallback doesn't exist, we already notified the Client about
// this item. That means something went wrong, so stop here.
if (start_callbacks_.find(guid) == start_callbacks_.end())
return;
if (!success) {
HandleStartDownloadResponse(client, guid,
DownloadParams::StartResult::INTERNAL_ERROR);
return;
}
HandleStartDownloadResponse(client, guid,
DownloadParams::StartResult::ACCEPTED);
Entry* entry = model_->Get(guid);
DCHECK(entry);
DCHECK_EQ(Entry::State::NEW, entry->state);
TransitTo(entry, Entry::State::AVAILABLE, model_.get());
ActivateMoreDownloads();
}
void ControllerImpl::OnItemUpdated(bool success,
DownloadClient client,
const std::string& guid) {
Entry* entry = model_->Get(guid);
DCHECK(entry);
// Now that we're sure that our state is set correctly, it is OK to remove the
// DriverEntry. If we restart we'll see a COMPLETE state and handle it
// accordingly.
if (entry->state == Entry::State::COMPLETE)
driver_->Remove(guid);
// TODO(dtrainor): If failed, clean up any download state accordingly.
}
void ControllerImpl::OnItemRemoved(bool success,
DownloadClient client,
const std::string& guid) {
// TODO(dtrainor): If failed, clean up any download state accordingly.
}
void ControllerImpl::OnDeviceStatusChanged(const DeviceStatus& device_status) {
UpdateDriverStates();
ActivateMoreDownloads();
}
void ControllerImpl::AttemptToFinalizeSetup() {
if (!startup_status_.Complete())
return;
stats::LogControllerStartupStatus(startup_status_);
if (!startup_status_.Ok()) {
// TODO(dtrainor): Recover here. Try to clean up any disk state and, if
// possible, any DownloadDriver data and continue with initialization?
// If we cannot recover, notify Clients that the service is unavailable.
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::Bind(&ControllerImpl::SendOnServiceUnavailable,
weak_ptr_factory_.GetWeakPtr()));
NotifyServiceOfStartup();
return;
}
device_status_listener_->Start(this);
PollActiveDriverDownloads();
CancelOrphanedRequests();
CleanupUnknownFiles();
RemoveCleanupEligibleDownloads();
ResolveInitialRequestStates();
NotifyClientsOfStartup();
initializing_internals_ = false;
UpdateDriverStates();
KillTimedOutDownloads();
NotifyServiceOfStartup();
// Pull the initial straw if active downloads haven't reach maximum.
ActivateMoreDownloads();
}
void ControllerImpl::PollActiveDriverDownloads() {
std::set<std::string> guids = driver_->GetActiveDownloads();
for (auto guid : guids) {
if (!model_->Get(guid))
externally_active_downloads_.insert(guid);
}
}
void ControllerImpl::CancelOrphanedRequests() {
auto entries = model_->PeekEntries();
std::vector<std::string> guids_to_remove;
std::set<base::FilePath> files_to_remove;
for (auto* entry : entries) {
if (!clients_->GetClient(entry->client)) {
guids_to_remove.push_back(entry->guid);
files_to_remove.insert(entry->target_file_path);
}
}
for (const auto& guid : guids_to_remove) {
driver_->Remove(guid);
model_->Remove(guid);
}
file_monitor_->DeleteFiles(files_to_remove,
stats::FileCleanupReason::ORPHANED);
}
void ControllerImpl::CleanupUnknownFiles() {
auto entries = model_->PeekEntries();
std::vector<DriverEntry> driver_entries;
for (auto* entry : entries) {
base::Optional<DriverEntry> driver_entry = driver_->Find(entry->guid);
if (driver_entry.has_value())
driver_entries.push_back(driver_entry.value());
}
file_monitor_->DeleteUnknownFiles(entries, driver_entries);
}
void ControllerImpl::ResolveInitialRequestStates() {
auto entries = model_->PeekEntries();
for (auto* entry : entries) {
// Pull the initial Entry::State and DriverEntry::State.
Entry::State state = entry->state;
auto driver_entry = driver_->Find(entry->guid);
base::Optional<DriverEntry::State> driver_state;
if (driver_entry.has_value()) {
DCHECK_NE(DriverEntry::State::UNKNOWN, driver_entry->state);
driver_state = driver_entry->state;
}
// Determine what the new Entry::State should be based on the two original
// states of the two different systems.
Entry::State new_state = state;
switch (state) {
case Entry::State::NEW:
// This means we shut down but may have not ACK'ed the download. That
// is OK, we will still notify the Client about the GUID when we send
// them our initialize method.
new_state = Entry::State::AVAILABLE;
break;
case Entry::State::COMPLETE:
// We're already in our end state. Just stay here.
new_state = Entry::State::COMPLETE;
break;
case Entry::State::AVAILABLE: // Intentional fallthrough.
case Entry::State::ACTIVE: // Intentional fallthrough.
case Entry::State::PAUSED: {
// All three of these states are effectively driven by the DriverEntry
// state.
if (!driver_state.has_value()) {
// If we don't have a DriverEntry::State, just leave the state alone.
new_state = state;
break;
}
// If we have a real DriverEntry::State, we need to determine which of
// those states makes sense for our Entry. Our entry can either be in
// two states now: It's effective 'active' state (ACTIVE or PAUSED) or
// COMPLETE.
bool is_paused = state == Entry::State::PAUSED;
Entry::State active =
is_paused ? Entry::State::PAUSED : Entry::State::ACTIVE;
switch (driver_state.value()) {
case DriverEntry::State::IN_PROGRESS: // Intentional fallthrough.
case DriverEntry::State::INTERRUPTED:
// The DriverEntry isn't done, so we need to set the Entry to the
// 'active' state.
new_state = active;
break;
case DriverEntry::State::COMPLETE: // Intentional fallthrough.
// TODO(dtrainor, xingliu) Revisit this CANCELLED state to make sure
// all embedders behave properly.
case DriverEntry::State::CANCELLED:
// The DriverEntry is done. We need to set the Entry to the
// COMPLETE state.
new_state = Entry::State::COMPLETE;
break;
default:
NOTREACHED();
break;
}
break;
}
default:
NOTREACHED();
break;
}
// Update the Entry::State to the new correct state.
if (new_state != entry->state) {
stats::LogRecoveryOperation(new_state);
TransitTo(entry, new_state, model_.get());
}
// Given the new correct state, update the DriverEntry to reflect the Entry.
switch (new_state) {
case Entry::State::NEW: // Intentional fallthrough.
case Entry::State::AVAILABLE: // Intentional fallthrough.
// We should not have a DriverEntry here.
if (driver_entry.has_value())
driver_->Remove(entry->guid);
break;
case Entry::State::ACTIVE: // Intentional fallthrough.
case Entry::State::PAUSED:
// We're in the correct state. Let UpdateDriverStates() restart us if
// it wants to.
break;
case Entry::State::COMPLETE:
if (state != Entry::State::COMPLETE) {
// We are changing states to COMPLETE. Handle this like a normal
// completed download.
// Treat CANCELLED and INTERRUPTED as failures. We have to assume the
// DriverEntry might not have persisted in time.
CompletionType completion_type =
(!driver_entry.has_value() ||
driver_entry->state == DriverEntry::State::CANCELLED ||
driver_entry->state == DriverEntry::State::INTERRUPTED)
? CompletionType::UNKNOWN
: CompletionType::SUCCEED;
HandleCompleteDownload(completion_type, entry->guid);
} else {
// We're staying in COMPLETE. Make sure there is no DriverEntry here.
if (driver_entry.has_value())
driver_->Remove(entry->guid);
}
break;
}
}
}
void ControllerImpl::UpdateDriverStates() {
DCHECK(startup_status_.Complete());
for (auto* entry : model_->PeekEntries())
UpdateDriverState(entry);
}
void ControllerImpl::UpdateDriverState(Entry* entry) {
DCHECK(!initializing_internals_);
if (entry->state != Entry::State::ACTIVE &&
entry->state != Entry::State::PAUSED) {
return;
}
// This method will need to figure out what to do with a failed download and
// either a) restart it or b) fail the download.
base::Optional<DriverEntry> driver_entry = driver_->Find(entry->guid);
bool meets_device_criteria = device_status_listener_->CurrentDeviceStatus()
.MeetsCondition(entry->scheduling_params)
.MeetsRequirements();
bool force_pause =
!externally_active_downloads_.empty() &&
entry->scheduling_params.priority != SchedulingParams::Priority::UI;
bool entry_paused = entry->state == Entry::State::PAUSED;
bool pause_driver = entry_paused || force_pause || !meets_device_criteria;
if (pause_driver) {
if (driver_entry.has_value())
driver_->Pause(entry->guid);
} else {
bool is_new_attempt =
!driver_entry.has_value() ||
driver_entry->state == DriverEntry::State::INTERRUPTED;
if (is_new_attempt) {
entry->attempt_count++;
model_->Update(*entry);
if (entry->attempt_count >= config_->max_retry_count) {
HandleCompleteDownload(CompletionType::FAIL, entry->guid);
return;
}
}
if (driver_entry.has_value()) {
driver_->Resume(entry->guid);
} else {
driver_->Start(entry->request_params, entry->guid,
entry->target_file_path, NO_TRAFFIC_ANNOTATION_YET);
}
}
}
void ControllerImpl::NotifyClientsOfStartup() {
std::set<Entry::State> ignored_states = {Entry::State::COMPLETE};
auto categorized = util::MapEntriesToClients(
clients_->GetRegisteredClients(), model_->PeekEntries(), ignored_states);
for (auto client_id : clients_->GetRegisteredClients()) {
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::Bind(&ControllerImpl::SendOnServiceInitialized,
weak_ptr_factory_.GetWeakPtr(), client_id,
categorized[client_id]));
}
}
void ControllerImpl::NotifyServiceOfStartup() {
if (init_callback_.is_null())
return;
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::ResetAndReturn(&init_callback_));
}
void ControllerImpl::HandleStartDownloadResponse(
DownloadClient client,
const std::string& guid,
DownloadParams::StartResult result) {
auto callback = start_callbacks_[guid];
start_callbacks_.erase(guid);
HandleStartDownloadResponse(client, guid, result, callback);
}
void ControllerImpl::HandleStartDownloadResponse(
DownloadClient client,
const std::string& guid,
DownloadParams::StartResult result,
const DownloadParams::StartCallback& callback) {
stats::LogStartDownloadResult(client, result);
// UNEXPECTED_GUID means the guid was already in use. Don't remove this entry
// from the model because it's there due to another request.
if (result != DownloadParams::StartResult::ACCEPTED &&
result != DownloadParams::StartResult::UNEXPECTED_GUID &&
model_->Get(guid) != nullptr) {
model_->Remove(guid);
}
if (callback.is_null())
return;
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::Bind(callback, guid, result));
}
void ControllerImpl::HandleCompleteDownload(CompletionType type,
const std::string& guid) {
Entry* entry = model_->Get(guid);
DCHECK(entry);
stats::LogDownloadCompletion(type, entry->attempt_count);
if (entry->state == Entry::State::COMPLETE) {
DVLOG(1) << "Download is already completed.";
return;
}
if (type == CompletionType::SUCCEED) {
auto driver_entry = driver_->Find(guid);
DCHECK(driver_entry.has_value());
if (driver_entry->current_file_path != entry->target_file_path) {
stats::LogFilePathsAreStrangelyDifferent();
entry->target_file_path = driver_entry->current_file_path;
}
entry->completion_time = driver_entry->completion_time;
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::Bind(&ControllerImpl::SendOnDownloadSucceeded,
weak_ptr_factory_.GetWeakPtr(), entry->client,
guid, driver_entry->current_file_path,
driver_entry->bytes_downloaded));
TransitTo(entry, Entry::State::COMPLETE, model_.get());
ScheduleCleanupTask();
} else {
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::Bind(&ControllerImpl::SendOnDownloadFailed,
weak_ptr_factory_.GetWeakPtr(), entry->client,
guid, FailureReasonFromCompletionType(type)));
// TODO(dtrainor): Handle the case where we crash before the model write
// happens and we have no driver entry.
driver_->Remove(entry->guid);
model_->Remove(guid);
}
ActivateMoreDownloads();
}
void ControllerImpl::ScheduleCleanupTask() {
base::Time earliest_completion_time = base::Time::Max();
for (const Entry* entry : model_->PeekEntries()) {
if (entry->completion_time == base::Time() ||
entry->state != Entry::State::COMPLETE)
continue;
if (entry->completion_time < earliest_completion_time) {
earliest_completion_time = entry->completion_time;
}
}
if (earliest_completion_time == base::Time::Max())
return;
base::TimeDelta start_time = earliest_completion_time +
config_->file_keep_alive_time -
base::Time::Now();
base::TimeDelta end_time = start_time + config_->file_cleanup_window;
task_scheduler_->ScheduleTask(DownloadTaskType::CLEANUP_TASK, false, false,
start_time.InSeconds(), end_time.InSeconds());
}
void ControllerImpl::ScheduleKillDownloadTaskIfNecessary() {
base::Time earliest_cancel_time = base::Time::Max();
for (const Entry* entry : model_->PeekEntries()) {
if (entry->state != Entry::State::COMPLETE &&
entry->scheduling_params.cancel_time < earliest_cancel_time) {
earliest_cancel_time = entry->scheduling_params.cancel_time;
}
}
if (earliest_cancel_time == base::Time::Max())
return;
base::TimeDelta time_to_cancel =
earliest_cancel_time > base::Time::Now()
? earliest_cancel_time - base::Time::Now()
: base::TimeDelta();
cancel_downloads_callback_.Reset(base::Bind(
&ControllerImpl::KillTimedOutDownloads, weak_ptr_factory_.GetWeakPtr()));
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE, cancel_downloads_callback_.callback(), time_to_cancel);
}
void ControllerImpl::KillTimedOutDownloads() {
for (const Entry* entry : model_->PeekEntries()) {
if (entry->state != Entry::State::COMPLETE &&
entry->scheduling_params.cancel_time <= base::Time::Now()) {
HandleCompleteDownload(CompletionType::TIMEOUT, entry->guid);
}
}
ScheduleKillDownloadTaskIfNecessary();
}
void ControllerImpl::ActivateMoreDownloads() {
if (initializing_internals_)
return;
// Check all the entries and the configuration to throttle number of
// downloads.
std::map<Entry::State, uint32_t> entries_states;
Model::EntryList scheduling_candidates;
for (auto* const entry : model_->PeekEntries()) {
entries_states[entry->state]++;
// Only schedule background tasks based on available and active entries.
if (entry->state == Entry::State::AVAILABLE ||
entry->state == Entry::State::ACTIVE) {
scheduling_candidates.emplace_back(entry);
}
}
uint32_t paused_count = entries_states[Entry::State::PAUSED];
uint32_t active_count = entries_states[Entry::State::ACTIVE];
bool has_actionable_downloads = false;
while (CanActivateMoreDownloads(config_, active_count, paused_count)) {
Entry* next = scheduler_->Next(
model_->PeekEntries(), device_status_listener_->CurrentDeviceStatus());
if (!next)
break;
has_actionable_downloads = true;
DCHECK_EQ(Entry::State::AVAILABLE, next->state);
TransitTo(next, Entry::State::ACTIVE, model_.get());
active_count++;
UpdateDriverState(next);
}
if (!has_actionable_downloads) {
HandleTaskFinished(DownloadTaskType::DOWNLOAD_TASK, false,
stats::ScheduledTaskStatus::COMPLETED_NORMALLY);
}
scheduler_->Reschedule(scheduling_candidates);
}
void ControllerImpl::HandleExternalDownload(const std::string& guid,
bool active) {
if (active) {
externally_active_downloads_.insert(guid);
} else {
externally_active_downloads_.erase(guid);
}
UpdateDriverStates();
}
void ControllerImpl::SendOnServiceInitialized(
DownloadClient client_id,
const std::vector<std::string>& guids) {
auto* client = clients_->GetClient(client_id);
DCHECK(client);
client->OnServiceInitialized(guids);
}
void ControllerImpl::SendOnServiceUnavailable() {
for (auto client_id : clients_->GetRegisteredClients()) {
clients_->GetClient(client_id)->OnServiceUnavailable();
}
}
void ControllerImpl::SendOnDownloadUpdated(DownloadClient client_id,
const std::string& guid,
uint64_t bytes_downloaded) {
if (!model_->Get(guid))
return;
auto* client = clients_->GetClient(client_id);
DCHECK(client);
client->OnDownloadUpdated(guid, bytes_downloaded);
}
void ControllerImpl::SendOnDownloadSucceeded(DownloadClient client_id,
const std::string& guid,
const base::FilePath& path,
uint64_t size) {
auto* client = clients_->GetClient(client_id);
DCHECK(client);
client->OnDownloadSucceeded(guid, path, size);
}
void ControllerImpl::SendOnDownloadFailed(
DownloadClient client_id,
const std::string& guid,
download::Client::FailureReason reason) {
auto* client = clients_->GetClient(client_id);
DCHECK(client);
client->OnDownloadFailed(guid, reason);
}
} // namespace download