diff options
Diffstat (limited to 'chromium/components/download')
92 files changed, 8701 insertions, 1180 deletions
diff --git a/chromium/components/download/BUILD.gn b/chromium/components/download/BUILD.gn index 618a7055c8b..441c1be542d 100644 --- a/chromium/components/download/BUILD.gn +++ b/chromium/components/download/BUILD.gn @@ -2,25 +2,24 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -if (is_android) { - import("//build/config/android/config.gni") - import("//build/config/android/rules.gni") -} +# TODO(dtrainor): Remove this file and have components_unittests depend on +# //components/download/internal:unit_tests directly. +group("unit_tests") { + testonly = true -group("download") { - public_deps = [ - "//components/download/public", + deps = [ + "//components/download/internal:unit_tests", ] - deps = [ - "//components/download/internal", + data_deps = [ + ":components_unittests_gtest_filter", ] } -group("unit_tests") { +source_set("components_unittests_gtest_filter") { testonly = true - deps = [ - "//components/download/internal:unit_tests", + data = [ + "components_unittests.filter", ] } diff --git a/chromium/components/download/components_unittests.filter b/chromium/components/download/components_unittests.filter new file mode 100644 index 00000000000..f694bb67279 --- /dev/null +++ b/chromium/components/download/components_unittests.filter @@ -0,0 +1,14 @@ +AllDownloadItemNotifierTest.* +DeviceStatusListenerTest.* +DownloadDriverImplTest.* +DownloadSchedulerImplTest.* +DownloadServiceClientSetTest.* +DownloadServiceControllerImplTest.* +DownloadServiceImplTest.* +DownloadServiceEntryUtilsTest.* +DownloadServiceModelImplTest.* +DownloadStoreTest.* +FileMonitorTest.* +NetworkListenerTest.* +ProtoConversionsTest.* +ServiceConfigImplTest.*
\ No newline at end of file diff --git a/chromium/components/download/content/BUILD.gn b/chromium/components/download/content/BUILD.gn index 7ca79f683b1..049294f31a1 100644 --- a/chromium/components/download/content/BUILD.gn +++ b/chromium/components/download/content/BUILD.gn @@ -2,41 +2,13 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -if (is_android) { - import("//build/config/android/config.gni") - import("//build/config/android/rules.gni") -} - -source_set("content") { - sources = [ - "download_driver_impl.cc", - "download_driver_impl.h", - ] - - public_deps = [ - "//components/download/internal", - "//components/download/public", - ] - - deps = [ - "//base", - "//content/public/browser", - "//net", - "//url", - ] -} - -source_set("unit_tests") { +# TODO(dtrainor): Remove this file and have components_unittests depend on +# //components/download/content/internal:unit_tests directly. +group("unit_tests") { testonly = true - sources = [ - "download_driver_impl_unittest.cc", - ] - deps = [ - ":content", - "//content/test:test_support", - "//testing/gmock", - "//testing/gtest", + "//components/download/content/internal:unit_tests", + "//components/download/content/public:unit_tests", ] } diff --git a/chromium/components/download/content/DEPS b/chromium/components/download/content/DEPS index ea159c83af6..b72efab3087 100644 --- a/chromium/components/download/content/DEPS +++ b/chromium/components/download/content/DEPS @@ -1,4 +1,5 @@ include_rules = [ + "+components/leveldb_proto", "+content/public/browser", "+content/public/test", "+base", diff --git a/chromium/components/download/content/download_driver_impl.cc b/chromium/components/download/content/download_driver_impl.cc deleted file mode 100644 index 0d1d2811430..00000000000 --- a/chromium/components/download/content/download_driver_impl.cc +++ /dev/null @@ -1,188 +0,0 @@ -// 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/content/download_driver_impl.h" - -#include "components/download/internal/driver_entry.h" -#include "content/public/browser/download_interrupt_reasons.h" -#include "content/public/browser/download_url_parameters.h" -#include "content/public/browser/storage_partition.h" -#include "net/http/http_response_headers.h" - -namespace download { - -namespace { - -// Converts a content::DownloadItem::DownloadState to DriverEntry::State. -DriverEntry::State ToDriverEntryState( - content::DownloadItem::DownloadState state) { - switch (state) { - case content::DownloadItem::IN_PROGRESS: - return DriverEntry::State::IN_PROGRESS; - case content::DownloadItem::COMPLETE: - return DriverEntry::State::COMPLETE; - case content::DownloadItem::CANCELLED: - return DriverEntry::State::CANCELLED; - case content::DownloadItem::INTERRUPTED: - return DriverEntry::State::INTERRUPTED; - case content::DownloadItem::MAX_DOWNLOAD_STATE: - return DriverEntry::State::UNKNOWN; - default: - NOTREACHED(); - return DriverEntry::State::UNKNOWN; - } -} - -} // namespace - -// static -DriverEntry DownloadDriverImpl::CreateDriverEntry( - const content::DownloadItem* item) { - DCHECK(item); - DriverEntry entry; - entry.guid = item->GetGuid(); - entry.state = ToDriverEntryState(item->GetState()); - entry.paused = item->IsPaused(); - entry.bytes_downloaded = item->GetReceivedBytes(); - entry.expected_total_size = item->GetTotalBytes(); - entry.response_headers = item->GetResponseHeaders(); - return entry; -} - -DownloadDriverImpl::DownloadDriverImpl(content::DownloadManager* manager, - const base::FilePath& dir) - : download_manager_(manager), file_dir_(dir), client_(nullptr) { - DCHECK(download_manager_); -} - -DownloadDriverImpl::~DownloadDriverImpl() { - if (download_manager_) - download_manager_->RemoveObserver(this); -} - -void DownloadDriverImpl::Initialize(DownloadDriver::Client* client) { - DCHECK(!client_); - client_ = client; - DCHECK(client_); - - // |download_manager_| may be shut down. Informs the client. - if (!download_manager_) { - client_->OnDriverReady(false); - return; - } - - download_manager_->AddObserver(this); - if (download_manager_->IsManagerInitialized()) - client_->OnDriverReady(true); -} - -bool DownloadDriverImpl::IsReady() const { - return client_ && download_manager_; -} - -void DownloadDriverImpl::Start(const DownloadParams& params) { - DCHECK(!params.request_params.url.is_empty()); - DCHECK(!params.guid.empty()); - if (!download_manager_) - return; - - content::StoragePartition* storage_partition = - content::BrowserContext::GetStoragePartitionForSite( - download_manager_->GetBrowserContext(), params.request_params.url); - DCHECK(storage_partition); - - std::unique_ptr<content::DownloadUrlParameters> download_url_params( - new content::DownloadUrlParameters( - params.request_params.url, - storage_partition->GetURLRequestContext())); - - // TODO(xingliu): Handle the request headers from |params|, need to tweak - // download network stack. - // Make content::DownloadManager handle potential guid collision and return - // an error to fail the download cleanly. - download_url_params->set_guid(params.guid); - download_url_params->set_transient(true); - download_url_params->set_method(params.request_params.method); - download_url_params->set_file_path(file_dir_.AppendASCII(params.guid)); - - download_manager_->DownloadUrl(std::move(download_url_params)); -} - -void DownloadDriverImpl::Cancel(const std::string& guid) { - if (!download_manager_) - return; - content::DownloadItem* item = download_manager_->GetDownloadByGuid(guid); - // Cancels the download and removes the persisted records in content layer. - if (item) { - item->RemoveObserver(this); - item->Remove(); - } -} - -void DownloadDriverImpl::Pause(const std::string& guid) { - if (!download_manager_) - return; - content::DownloadItem* item = download_manager_->GetDownloadByGuid(guid); - if (item) - item->Pause(); -} - -void DownloadDriverImpl::Resume(const std::string& guid) { - if (!download_manager_) - return; - content::DownloadItem* item = download_manager_->GetDownloadByGuid(guid); - if (item) - item->Resume(); -} - -base::Optional<DriverEntry> DownloadDriverImpl::Find(const std::string& guid) { - if (!download_manager_) - return base::nullopt; - content::DownloadItem* item = download_manager_->GetDownloadByGuid(guid); - if (item) - return CreateDriverEntry(item); - return base::nullopt; -} - -void DownloadDriverImpl::OnDownloadUpdated(content::DownloadItem* item) { - DCHECK(client_); - - using DownloadState = content::DownloadItem::DownloadState; - DownloadState state = item->GetState(); - content::DownloadInterruptReason reason = item->GetLastReason(); - DriverEntry entry = CreateDriverEntry(item); - - if (state == DownloadState::COMPLETE) { - client_->OnDownloadSucceeded(entry, item->GetTargetFilePath()); - item->RemoveObserver(this); - } else if (state == DownloadState::IN_PROGRESS) { - client_->OnDownloadUpdated(entry); - } else if (reason != - content::DownloadInterruptReason::DOWNLOAD_INTERRUPT_REASON_NONE) { - client_->OnDownloadFailed(entry, static_cast<int>(reason)); - item->RemoveObserver(this); - } -} - -void DownloadDriverImpl::OnDownloadCreated(content::DownloadManager* manager, - content::DownloadItem* item) { - // Listens to all downloads. - item->AddObserver(this); - DCHECK(client_); - DriverEntry entry = CreateDriverEntry(item); - client_->OnDownloadCreated(entry); -} - -void DownloadDriverImpl::OnManagerInitialized() { - DCHECK(client_); - DCHECK(download_manager_); - client_->OnDriverReady(true); -} - -void DownloadDriverImpl::ManagerGoingDown(content::DownloadManager* manager) { - DCHECK_EQ(download_manager_, manager); - download_manager_ = nullptr; -} - -} // namespace download diff --git a/chromium/components/download/content/download_driver_impl.h b/chromium/components/download/content/download_driver_impl.h deleted file mode 100644 index c7c5b1ce1b2..00000000000 --- a/chromium/components/download/content/download_driver_impl.h +++ /dev/null @@ -1,67 +0,0 @@ -// 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. - -#ifndef COMPONENTS_DOWNLOAD_CONTENT_DOWNLOAD_DRIVER_IMPL_H_ -#define COMPONENTS_DOWNLOAD_CONTENT_DOWNLOAD_DRIVER_IMPL_H_ - -#include <string> - -#include "base/files/file_path.h" -#include "components/download/internal/download_driver.h" -#include "components/download/public/download_params.h" -#include "content/public/browser/browser_context.h" -#include "content/public/browser/download_manager.h" - -namespace download { - -struct DriverEntry; - -// Aggregates and handles all interaction between download service and content -// download logic. -class DownloadDriverImpl : public DownloadDriver, - public content::DownloadManager::Observer, - public content::DownloadItem::Observer { - public: - // Creates a driver entry based on a download item. - static DriverEntry CreateDriverEntry(const content::DownloadItem* item); - - // Create the driver. All files downloaded will be saved to |dir|. - DownloadDriverImpl(content::DownloadManager* manager, - const base::FilePath& dir); - ~DownloadDriverImpl() override; - - // DownloadDriver implementation. - void Initialize(DownloadDriver::Client* client) override; - bool IsReady() const override; - void Start(const DownloadParams& params) override; - void Cancel(const std::string& guid) override; - void Pause(const std::string& guid) override; - void Resume(const std::string& guid) override; - base::Optional<DriverEntry> Find(const std::string& guid) override; - - private: - // content::DownloadItem::Observer implementation. - void OnDownloadUpdated(content::DownloadItem* item) override; - - // content::DownloadManager::Observer implementation. - void OnDownloadCreated(content::DownloadManager* manager, - content::DownloadItem* item) override; - void OnManagerInitialized() override; - void ManagerGoingDown(content::DownloadManager* manager) override; - - // Low level download handle. - content::DownloadManager* download_manager_; - - // Target directory of download files. - base::FilePath file_dir_; - - // The client that receives updates from low level download logic. - DownloadDriver::Client* client_; - - DISALLOW_COPY_AND_ASSIGN(DownloadDriverImpl); -}; - -} // namespace download - -#endif // COMPONENTS_DOWNLOAD_CONTENT_DOWNLOAD_DRIVER_IMPL_H_ diff --git a/chromium/components/download/content/download_driver_impl_unittest.cc b/chromium/components/download/content/download_driver_impl_unittest.cc deleted file mode 100644 index c53b490fac4..00000000000 --- a/chromium/components/download/content/download_driver_impl_unittest.cc +++ /dev/null @@ -1,129 +0,0 @@ -// 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/content/download_driver_impl.h" - -#include <memory> -#include <string> - -#include "base/memory/ptr_util.h" -#include "content/public/test/fake_download_item.h" -#include "content/public/test/mock_download_manager.h" -#include "net/http/http_response_headers.h" -#include "testing/gmock/include/gmock/gmock.h" -#include "testing/gtest/include/gtest/gtest.h" - -using testing::_; -using testing::NiceMock; -using testing::Return; - -namespace download { - -namespace { - -const char kFakeGuid[] = "fake_guid"; - -// Matcher to compare driver entries. Not all the memeber fields are compared. -// Currently no comparison in non test code, so no operator== override for -// driver entry. -MATCHER_P(DriverEntryEuqual, entry, "") { - return entry.guid == arg.guid && entry.state == arg.state && - entry.bytes_downloaded == arg.bytes_downloaded; -} - -} // namespace - -class MockDriverClient : public DownloadDriver::Client { - public: - MOCK_METHOD1(OnDriverReady, void(bool)); - MOCK_METHOD1(OnDownloadCreated, void(const DriverEntry&)); - MOCK_METHOD2(OnDownloadFailed, void(const DriverEntry&, int)); - MOCK_METHOD2(OnDownloadSucceeded, - void(const DriverEntry&, const base::FilePath&)); - MOCK_METHOD1(OnDownloadUpdated, void(const DriverEntry&)); -}; - -class DownloadDriverImplTest : public testing::Test { - public: - DownloadDriverImplTest() = default; - ~DownloadDriverImplTest() override = default; - - void SetUp() override { - driver_ = - base::MakeUnique<DownloadDriverImpl>(&mock_manager_, base::FilePath()); - } - - // TODO(xingliu): implements test download manager for embedders to test. - NiceMock<content::MockDownloadManager> mock_manager_; - MockDriverClient mock_client_; - std::unique_ptr<DownloadDriverImpl> driver_; - - private: - DISALLOW_COPY_AND_ASSIGN(DownloadDriverImplTest); -}; - -// Ensure the download manager can be initialized after the download driver. -TEST_F(DownloadDriverImplTest, ManagerLateInitialization) { - EXPECT_CALL(mock_manager_, IsManagerInitialized()) - .Times(1) - .WillOnce(Return(false)); - driver_->Initialize(&mock_client_); - - EXPECT_CALL(mock_client_, OnDriverReady(true)); - static_cast<content::DownloadManager::Observer*>(driver_.get()) - ->OnManagerInitialized(); -} - -// Ensures download updates from download items are propagated correctly. -TEST_F(DownloadDriverImplTest, DownloadItemUpdateEvents) { - using DownloadState = content::DownloadItem::DownloadState; - using DownloadInterruptReason = content::DownloadInterruptReason; - - EXPECT_CALL(mock_manager_, IsManagerInitialized()) - .Times(1) - .WillOnce(Return(true)); - EXPECT_CALL(mock_client_, OnDriverReady(true)).Times(1); - driver_->Initialize(&mock_client_); - - content::FakeDownloadItem fake_item; - fake_item.SetState(DownloadState::IN_PROGRESS); - fake_item.SetGuid(kFakeGuid); - fake_item.SetReceivedBytes(0); - fake_item.SetTotalBytes(1024); - DriverEntry entry = DownloadDriverImpl::CreateDriverEntry(&fake_item); - - EXPECT_CALL(mock_client_, OnDownloadUpdated(DriverEntryEuqual(entry))) - .Times(1) - .RetiresOnSaturation(); - static_cast<content::DownloadItem::Observer*>(driver_.get()) - ->OnDownloadUpdated(&fake_item); - - // Nothing happens for cancelled state. - fake_item.SetState(DownloadState::CANCELLED); - static_cast<content::DownloadItem::Observer*>(driver_.get()) - ->OnDownloadUpdated(&fake_item); - - fake_item.SetReceivedBytes(1024); - fake_item.SetState(DownloadState::COMPLETE); - entry = DownloadDriverImpl::CreateDriverEntry(&fake_item); - EXPECT_CALL(mock_client_, OnDownloadSucceeded(DriverEntryEuqual(entry), _)) - .Times(1) - .RetiresOnSaturation(); - static_cast<content::DownloadItem::Observer*>(driver_.get()) - ->OnDownloadUpdated(&fake_item); - - fake_item.SetState(DownloadState::INTERRUPTED); - fake_item.SetLastReason( - DownloadInterruptReason::DOWNLOAD_INTERRUPT_REASON_SERVER_BAD_CONTENT); - entry = DownloadDriverImpl::CreateDriverEntry(&fake_item); - int reason = static_cast<int>( - DownloadInterruptReason::DOWNLOAD_INTERRUPT_REASON_SERVER_BAD_CONTENT); - EXPECT_CALL(mock_client_, OnDownloadFailed(DriverEntryEuqual(entry), reason)) - .Times(1) - .RetiresOnSaturation(); - static_cast<content::DownloadItem::Observer*>(driver_.get()) - ->OnDownloadUpdated(&fake_item); -} - -} // namespace download diff --git a/chromium/components/download/content/factory/BUILD.gn b/chromium/components/download/content/factory/BUILD.gn new file mode 100644 index 00000000000..46f834f0296 --- /dev/null +++ b/chromium/components/download/content/factory/BUILD.gn @@ -0,0 +1,25 @@ +# 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. + +source_set("factory") { + sources = [ + "download_service_factory.cc", + "download_service_factory.h", + ] + + public_deps = [ + "//components/download/public", + ] + + deps = [ + "//base", + "//components/download/content/internal", + "//components/download/internal", + "//components/download/internal/proto", + "//components/leveldb_proto", + "//content/public/browser", + "//net", + "//url", + ] +} diff --git a/chromium/components/download/content/factory/download_service_factory.cc b/chromium/components/download/content/factory/download_service_factory.cc new file mode 100644 index 00000000000..95fbf6ade1d --- /dev/null +++ b/chromium/components/download/content/factory/download_service_factory.cc @@ -0,0 +1,58 @@ +// 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 "base/files/file_path.h" +#include "base/memory/ptr_util.h" +#include "components/download/content/internal/download_driver_impl.h" +#include "components/download/internal/client_set.h" +#include "components/download/internal/config.h" +#include "components/download/internal/controller_impl.h" +#include "components/download/internal/download_service_impl.h" +#include "components/download/internal/download_store.h" +#include "components/download/internal/file_monitor_impl.h" +#include "components/download/internal/model_impl.h" +#include "components/download/internal/proto/entry.pb.h" +#include "components/download/internal/scheduler/scheduler_impl.h" +#include "components/leveldb_proto/proto_database_impl.h" + +namespace download { +namespace { +const base::FilePath::CharType kEntryDBStorageDir[] = + FILE_PATH_LITERAL("EntryDB"); +const base::FilePath::CharType kFilesStorageDir[] = FILE_PATH_LITERAL("Files"); +} // namespace + +DownloadService* CreateDownloadService( + std::unique_ptr<DownloadClientMap> clients, + content::DownloadManager* download_manager, + const base::FilePath& storage_dir, + const scoped_refptr<base::SequencedTaskRunner>& background_task_runner, + std::unique_ptr<TaskScheduler> task_scheduler) { + auto client_set = base::MakeUnique<ClientSet>(std::move(clients)); + auto config = Configuration::CreateFromFinch(); + + auto files_storage_dir = storage_dir.Append(kFilesStorageDir); + auto driver = base::MakeUnique<DownloadDriverImpl>(download_manager); + + auto entry_db_storage_dir = storage_dir.Append(kEntryDBStorageDir); + auto entry_db = + base::MakeUnique<leveldb_proto::ProtoDatabaseImpl<protodb::Entry>>( + background_task_runner); + auto store = base::MakeUnique<DownloadStore>(entry_db_storage_dir, + std::move(entry_db)); + auto model = base::MakeUnique<ModelImpl>(std::move(store)); + auto device_status_listener = + base::MakeUnique<DeviceStatusListener>(config->network_change_delay); + auto scheduler = base::MakeUnique<SchedulerImpl>( + task_scheduler.get(), config.get(), client_set.get()); + auto file_monitor = base::MakeUnique<FileMonitorImpl>( + files_storage_dir, background_task_runner, config->file_keep_alive_time); + auto controller = base::MakeUnique<ControllerImpl>( + config.get(), std::move(client_set), std::move(driver), std::move(model), + std::move(device_status_listener), std::move(scheduler), + std::move(task_scheduler), std::move(file_monitor), files_storage_dir); + return new DownloadServiceImpl(std::move(config), std::move(controller)); +} + +} // namespace download diff --git a/chromium/components/download/content/factory/download_service_factory.h b/chromium/components/download/content/factory/download_service_factory.h new file mode 100644 index 00000000000..8df6dfe7da5 --- /dev/null +++ b/chromium/components/download/content/factory/download_service_factory.h @@ -0,0 +1,43 @@ +// 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. + +#ifndef COMPONENTS_DOWNLOAD_CONTENT_FACTORY_DOWNLOAD_SERVICE_FACTORY_H_ +#define COMPONENTS_DOWNLOAD_CONTENT_FACTORY_DOWNLOAD_SERVICE_FACTORY_H_ + +#include <memory> + +#include "base/files/file_path.h" +#include "base/memory/ref_counted.h" +#include "base/sequenced_task_runner.h" +#include "components/download/public/clients.h" + +namespace content { +class DownloadManager; +} // namespace content + +namespace download { + +class DownloadService; +class TaskScheduler; + +// |clients| is a map of DownloadClient -> std::unique_ptr<Client>. This +// represents all of the clients that are allowed to have requests made on +// their behalf. This cannot be changed after startup. Any existing requests +// no longer associated with a client will be cancelled. +// |storage_dir| is a path to where all the local storage will be. This will +// hold the internal database as well as any temporary files on disk. If this +// is an empty path, the service will not persist any information to disk and +// will act as an in-memory only service (this means no auto-retries after +// restarts, no files written on completion, etc.). +// |background_task_runner| will be used for all disk reads and writes. +DownloadService* CreateDownloadService( + std::unique_ptr<DownloadClientMap> clients, + content::DownloadManager* download_manager, + const base::FilePath& storage_dir, + const scoped_refptr<base::SequencedTaskRunner>& background_task_runner, + std::unique_ptr<TaskScheduler> task_scheduler); + +} // namespace download + +#endif // COMPONENTS_DOWNLOAD_CONTENT_FACTORY_DOWNLOAD_SERVICE_FACTORY_H_ diff --git a/chromium/components/download/content/internal/BUILD.gn b/chromium/components/download/content/internal/BUILD.gn new file mode 100644 index 00000000000..77025b91da8 --- /dev/null +++ b/chromium/components/download/content/internal/BUILD.gn @@ -0,0 +1,45 @@ +# 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. + +source_set("internal") { + visibility = [ + ":*", + "//components/download/content/factory", + ] + + sources = [ + "download_driver_impl.cc", + "download_driver_impl.h", + ] + + public_deps = [ + "//components/download/content/public", + "//components/download/internal", + "//components/download/public", + ] + + deps = [ + "//base", + "//content/public/browser", + "//net", + "//url", + ] +} + +source_set("unit_tests") { + testonly = true + + sources = [ + "download_driver_impl_unittest.cc", + ] + + deps = [ + ":internal", + "//base/test:test_support", + "//components/download/content/public", + "//content/test:test_support", + "//testing/gmock", + "//testing/gtest", + ] +} diff --git a/chromium/components/download/content/internal/download_driver_impl.cc b/chromium/components/download/content/internal/download_driver_impl.cc new file mode 100644 index 00000000000..99686e18454 --- /dev/null +++ b/chromium/components/download/content/internal/download_driver_impl.cc @@ -0,0 +1,279 @@ +// 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/content/internal/download_driver_impl.h" + +#include <set> +#include <vector> + +#include "base/memory/ptr_util.h" +#include "base/metrics/histogram_macros.h" +#include "base/threading/thread_task_runner_handle.h" +#include "components/download/internal/driver_entry.h" +#include "content/public/browser/download_interrupt_reasons.h" +#include "content/public/browser/download_url_parameters.h" +#include "content/public/browser/storage_partition.h" +#include "net/http/http_response_headers.h" + +namespace download { + +namespace { + +// Converts a content::DownloadItem::DownloadState to DriverEntry::State. +DriverEntry::State ToDriverEntryState( + content::DownloadItem::DownloadState state) { + switch (state) { + case content::DownloadItem::IN_PROGRESS: + return DriverEntry::State::IN_PROGRESS; + case content::DownloadItem::COMPLETE: + return DriverEntry::State::COMPLETE; + case content::DownloadItem::CANCELLED: + return DriverEntry::State::CANCELLED; + case content::DownloadItem::INTERRUPTED: + return DriverEntry::State::INTERRUPTED; + case content::DownloadItem::MAX_DOWNLOAD_STATE: + return DriverEntry::State::UNKNOWN; + default: + NOTREACHED(); + return DriverEntry::State::UNKNOWN; + } +} + +FailureType FailureTypeFromInterruptReason( + content::DownloadInterruptReason reason) { + switch (reason) { + case content::DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED: + case content::DOWNLOAD_INTERRUPT_REASON_FILE_NO_SPACE: + case content::DOWNLOAD_INTERRUPT_REASON_FILE_NAME_TOO_LONG: + case content::DOWNLOAD_INTERRUPT_REASON_FILE_TOO_LARGE: + case content::DOWNLOAD_INTERRUPT_REASON_FILE_VIRUS_INFECTED: + case content::DOWNLOAD_INTERRUPT_REASON_FILE_BLOCKED: + case content::DOWNLOAD_INTERRUPT_REASON_FILE_SECURITY_CHECK_FAILED: + case content::DOWNLOAD_INTERRUPT_REASON_SERVER_UNAUTHORIZED: + case content::DOWNLOAD_INTERRUPT_REASON_SERVER_CERT_PROBLEM: + case content::DOWNLOAD_INTERRUPT_REASON_SERVER_FORBIDDEN: + return FailureType::NOT_RECOVERABLE; + default: + return FailureType::RECOVERABLE; + } +} + +// Logs interrupt reason when download fails. +void LogDownloadInterruptReason(content::DownloadInterruptReason reason) { + UMA_HISTOGRAM_SPARSE_SLOWLY("Download.Service.Driver.InterruptReason", + reason); +} + +} // namespace + +// static +DriverEntry DownloadDriverImpl::CreateDriverEntry( + const content::DownloadItem* item) { + DCHECK(item); + DriverEntry entry; + entry.guid = item->GetGuid(); + entry.state = ToDriverEntryState(item->GetState()); + entry.paused = item->IsPaused(); + entry.bytes_downloaded = item->GetReceivedBytes(); + entry.expected_total_size = item->GetTotalBytes(); + entry.current_file_path = + item->GetState() == content::DownloadItem::DownloadState::COMPLETE + ? item->GetTargetFilePath() + : item->GetFullPath(); + entry.completion_time = item->GetEndTime(); + entry.response_headers = item->GetResponseHeaders(); + entry.url_chain = item->GetUrlChain(); + return entry; +} + +DownloadDriverImpl::DownloadDriverImpl(content::DownloadManager* manager) + : download_manager_(manager), client_(nullptr), weak_ptr_factory_(this) { + DCHECK(download_manager_); +} + +DownloadDriverImpl::~DownloadDriverImpl() = default; + +void DownloadDriverImpl::Initialize(DownloadDriver::Client* client) { + DCHECK(!client_); + client_ = client; + DCHECK(client_); + + // |download_manager_| may be shut down. Informs the client. + if (!download_manager_) { + client_->OnDriverReady(false); + return; + } + + notifier_ = + base::MakeUnique<AllDownloadItemNotifier>(download_manager_, this); +} + +void DownloadDriverImpl::HardRecover() { + // TODO(dtrainor, xingliu): Implement recovery for the DownloadManager. + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::Bind(&DownloadDriverImpl::OnHardRecoverComplete, + weak_ptr_factory_.GetWeakPtr(), true)); +} + +bool DownloadDriverImpl::IsReady() const { + return client_ && download_manager_ && + download_manager_->IsManagerInitialized(); +} + +void DownloadDriverImpl::Start( + const RequestParams& request_params, + const std::string& guid, + const base::FilePath& file_path, + const net::NetworkTrafficAnnotationTag& traffic_annotation) { + DCHECK(!request_params.url.is_empty()); + DCHECK(!guid.empty()); + if (!download_manager_) + return; + + content::StoragePartition* storage_partition = + content::BrowserContext::GetStoragePartitionForSite( + download_manager_->GetBrowserContext(), request_params.url); + DCHECK(storage_partition); + + std::unique_ptr<content::DownloadUrlParameters> download_url_params( + new content::DownloadUrlParameters( + request_params.url, storage_partition->GetURLRequestContext(), + traffic_annotation)); + + // TODO(xingliu): Make content::DownloadManager handle potential guid + // collision and return an error to fail the download cleanly. + for (net::HttpRequestHeaders::Iterator it(request_params.request_headers); + it.GetNext();) { + download_url_params->add_request_header(it.name(), it.value()); + } + download_url_params->set_guid(guid); + download_url_params->set_transient(true); + download_url_params->set_method(request_params.method); + download_url_params->set_file_path(file_path); + + download_manager_->DownloadUrl(std::move(download_url_params)); +} + +void DownloadDriverImpl::Remove(const std::string& guid) { + guid_to_remove_.emplace(guid); + + // DownloadItem::Remove will cause the item object removed from memory, post + // the remove task to avoid the object being accessed in the same call stack. + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::Bind(&DownloadDriverImpl::DoRemoveDownload, + weak_ptr_factory_.GetWeakPtr(), guid)); +} + +void DownloadDriverImpl::DoRemoveDownload(const std::string& guid) { + if (!download_manager_) + return; + content::DownloadItem* item = download_manager_->GetDownloadByGuid(guid); + // Cancels the download and removes the persisted records in content layer. + if (item) { + item->Remove(); + } +} + +void DownloadDriverImpl::Pause(const std::string& guid) { + if (!download_manager_) + return; + content::DownloadItem* item = download_manager_->GetDownloadByGuid(guid); + if (item) + item->Pause(); +} + +void DownloadDriverImpl::Resume(const std::string& guid) { + if (!download_manager_) + return; + content::DownloadItem* item = download_manager_->GetDownloadByGuid(guid); + if (item) + item->Resume(); +} + +base::Optional<DriverEntry> DownloadDriverImpl::Find(const std::string& guid) { + if (!download_manager_) + return base::nullopt; + content::DownloadItem* item = download_manager_->GetDownloadByGuid(guid); + if (item) + return CreateDriverEntry(item); + return base::nullopt; +} + +std::set<std::string> DownloadDriverImpl::GetActiveDownloads() { + std::set<std::string> guids; + if (!download_manager_) + return guids; + + std::vector<content::DownloadItem*> items; + download_manager_->GetAllDownloads(&items); + + for (auto* item : items) { + DriverEntry::State state = ToDriverEntryState(item->GetState()); + if (state == DriverEntry::State::IN_PROGRESS) + guids.insert(item->GetGuid()); + } + + return guids; +} + +void DownloadDriverImpl::OnDownloadUpdated(content::DownloadManager* manager, + content::DownloadItem* item) { + DCHECK(client_); + // Blocks the observer call if we asked to remove the download. + if (guid_to_remove_.find(item->GetGuid()) != guid_to_remove_.end()) + return; + + using DownloadState = content::DownloadItem::DownloadState; + DownloadState state = item->GetState(); + content::DownloadInterruptReason reason = item->GetLastReason(); + DriverEntry entry = CreateDriverEntry(item); + + if (state == DownloadState::COMPLETE) { + client_->OnDownloadSucceeded(entry); + } else if (state == DownloadState::IN_PROGRESS) { + client_->OnDownloadUpdated(entry); + } else if (reason != + content::DownloadInterruptReason::DOWNLOAD_INTERRUPT_REASON_NONE) { + LogDownloadInterruptReason(reason); + client_->OnDownloadFailed(entry, FailureTypeFromInterruptReason(reason)); + } +} + +void DownloadDriverImpl::OnDownloadRemoved(content::DownloadManager* manager, + content::DownloadItem* download) { + guid_to_remove_.erase(download->GetGuid()); + // |download| is about to be deleted. +} + +void DownloadDriverImpl::OnDownloadCreated(content::DownloadManager* manager, + content::DownloadItem* item) { + // Listens to all downloads. + DCHECK(client_); + DriverEntry entry = CreateDriverEntry(item); + + // Only notifies the client about new downloads. Existing download data will + // be loaded before the driver is ready. + if (IsReady()) + client_->OnDownloadCreated(entry); +} + +void DownloadDriverImpl::OnManagerInitialized( + content::DownloadManager* manager) { + DCHECK_EQ(download_manager_, manager); + DCHECK(client_); + DCHECK(download_manager_); + client_->OnDriverReady(true); +} + +void DownloadDriverImpl::OnManagerGoingDown(content::DownloadManager* manager) { + DCHECK_EQ(download_manager_, manager); + notifier_.reset(); + download_manager_ = nullptr; +} + +void DownloadDriverImpl::OnHardRecoverComplete(bool success) { + client_->OnDriverHardRecoverComplete(success); +} + +} // namespace download diff --git a/chromium/components/download/content/internal/download_driver_impl.h b/chromium/components/download/content/internal/download_driver_impl.h new file mode 100644 index 00000000000..e634862041f --- /dev/null +++ b/chromium/components/download/content/internal/download_driver_impl.h @@ -0,0 +1,88 @@ +// 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. + +#ifndef COMPONENTS_DOWNLOAD_CONTENT_INTERNAL_DOWNLOAD_DRIVER_IMPL_H_ +#define COMPONENTS_DOWNLOAD_CONTENT_INTERNAL_DOWNLOAD_DRIVER_IMPL_H_ + +#include <memory> +#include <set> +#include <string> + +#include "base/files/file_path.h" +#include "base/memory/weak_ptr.h" +#include "components/download/content/public/all_download_item_notifier.h" +#include "components/download/internal/download_driver.h" +#include "components/download/public/download_params.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/download_manager.h" +#include "net/traffic_annotation/network_traffic_annotation.h" + +namespace download { + +struct DriverEntry; + +// Aggregates and handles all interaction between download service and content +// download logic. +class DownloadDriverImpl : public DownloadDriver, + public AllDownloadItemNotifier::Observer { + public: + // Creates a driver entry based on a download item. + static DriverEntry CreateDriverEntry(const content::DownloadItem* item); + + // Create the driver. + DownloadDriverImpl(content::DownloadManager* manager); + ~DownloadDriverImpl() override; + + // DownloadDriver implementation. + void Initialize(DownloadDriver::Client* client) override; + void HardRecover() override; + bool IsReady() const override; + void Start( + const RequestParams& request_params, + const std::string& guid, + const base::FilePath& file_path, + const net::NetworkTrafficAnnotationTag& traffic_annotation) override; + void Remove(const std::string& guid) override; + void Pause(const std::string& guid) override; + void Resume(const std::string& guid) override; + base::Optional<DriverEntry> Find(const std::string& guid) override; + std::set<std::string> GetActiveDownloads() override; + + private: + // content::AllDownloadItemNotifier::Observer implementation. + void OnManagerInitialized(content::DownloadManager* manager) override; + void OnManagerGoingDown(content::DownloadManager* manager) override; + void OnDownloadCreated(content::DownloadManager* manager, + content::DownloadItem* item) override; + void OnDownloadUpdated(content::DownloadManager* manager, + content::DownloadItem* item) override; + void OnDownloadRemoved(content::DownloadManager* manager, + content::DownloadItem* item) override; + + void OnHardRecoverComplete(bool success); + + // Remove the download, used to be posted to the task queue. + void DoRemoveDownload(const std::string& guid); + + // Low level download handle. + content::DownloadManager* download_manager_; + + // The client that receives updates from low level download logic. + DownloadDriver::Client* client_; + + // Built lazily on initialize and destroyed when/if the manager is torn down. + std::unique_ptr<AllDownloadItemNotifier> notifier_; + + // Pending guid set of downloads that will be removed soon. + std::set<std::string> guid_to_remove_; + + // Only used to post tasks on the same thread. + base::WeakPtrFactory<DownloadDriverImpl> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(DownloadDriverImpl); +}; + +} // namespace download + +#endif // COMPONENTS_DOWNLOAD_CONTENT_INTERNAL_DOWNLOAD_DRIVER_IMPL_H_ diff --git a/chromium/components/download/content/internal/download_driver_impl_unittest.cc b/chromium/components/download/content/internal/download_driver_impl_unittest.cc new file mode 100644 index 00000000000..f4937e02d99 --- /dev/null +++ b/chromium/components/download/content/internal/download_driver_impl_unittest.cc @@ -0,0 +1,189 @@ +// 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/content/internal/download_driver_impl.h" + +#include <memory> +#include <string> + +#include "base/guid.h" +#include "base/memory/ptr_util.h" +#include "base/test/test_simple_task_runner.h" +#include "base/threading/thread_task_runner_handle.h" +#include "components/download/content/public/all_download_item_notifier.h" +#include "content/public/test/fake_download_item.h" +#include "content/public/test/mock_download_manager.h" +#include "net/http/http_response_headers.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using testing::_; +using testing::Invoke; +using testing::NiceMock; +using testing::Return; + +namespace download { + +namespace { + +ACTION_P(PopulateVector, items) { + arg0->insert(arg0->begin(), items.begin(), items.end()); +} + +const char kFakeGuid[] = "fake_guid"; + +// Matcher to compare driver entries. Not all the memeber fields are compared. +// Currently no comparison in non test code, so no operator== override for +// driver entry. +MATCHER_P(DriverEntryEqual, entry, "") { + return entry.guid == arg.guid && entry.state == arg.state && + entry.bytes_downloaded == arg.bytes_downloaded && + entry.current_file_path.value() == arg.current_file_path.value(); +} + +} // namespace + +class MockDriverClient : public DownloadDriver::Client { + public: + MOCK_METHOD1(OnDriverReady, void(bool)); + MOCK_METHOD1(OnDriverHardRecoverComplete, void(bool)); + MOCK_METHOD1(OnDownloadCreated, void(const DriverEntry&)); + MOCK_METHOD2(OnDownloadFailed, void(const DriverEntry&, FailureType)); + MOCK_METHOD1(OnDownloadSucceeded, void(const DriverEntry&)); + MOCK_METHOD1(OnDownloadUpdated, void(const DriverEntry&)); +}; + +class DownloadDriverImplTest : public testing::Test { + public: + DownloadDriverImplTest() + : task_runner_(new base::TestSimpleTaskRunner), handle_(task_runner_) {} + + ~DownloadDriverImplTest() override = default; + + void SetUp() override { + driver_ = base::MakeUnique<DownloadDriverImpl>(&mock_manager_); + } + + // TODO(xingliu): implements test download manager for embedders to test. + NiceMock<content::MockDownloadManager> mock_manager_; + MockDriverClient mock_client_; + std::unique_ptr<DownloadDriverImpl> driver_; + + protected: + scoped_refptr<base::TestSimpleTaskRunner> task_runner_; + base::ThreadTaskRunnerHandle handle_; + + private: + DISALLOW_COPY_AND_ASSIGN(DownloadDriverImplTest); +}; + +// Ensure the download manager can be initialized after the download driver. +TEST_F(DownloadDriverImplTest, ManagerLateInitialization) { + EXPECT_CALL(mock_manager_, IsManagerInitialized()) + .Times(1) + .WillOnce(Return(false)); + driver_->Initialize(&mock_client_); + + EXPECT_CALL(mock_client_, OnDriverReady(true)); + static_cast<AllDownloadItemNotifier::Observer*>(driver_.get()) + ->OnManagerInitialized(&mock_manager_); +} + +TEST_F(DownloadDriverImplTest, TestHardRecover) { + EXPECT_CALL(mock_manager_, IsManagerInitialized()) + .Times(1) + .WillOnce(Return(false)); + driver_->Initialize(&mock_client_); + + EXPECT_CALL(mock_client_, OnDriverHardRecoverComplete(true)).Times(1); + driver_->HardRecover(); + task_runner_->RunUntilIdle(); +} + +// Ensures download updates from download items are propagated correctly. +TEST_F(DownloadDriverImplTest, DownloadItemUpdateEvents) { + using DownloadState = content::DownloadItem::DownloadState; + using DownloadInterruptReason = content::DownloadInterruptReason; + + EXPECT_CALL(mock_manager_, IsManagerInitialized()) + .Times(1) + .WillOnce(Return(true)); + EXPECT_CALL(mock_client_, OnDriverReady(true)).Times(1); + driver_->Initialize(&mock_client_); + + content::FakeDownloadItem fake_item; + fake_item.SetState(DownloadState::IN_PROGRESS); + fake_item.SetGuid(kFakeGuid); + fake_item.SetReceivedBytes(0); + fake_item.SetTotalBytes(1024); + DriverEntry entry = DownloadDriverImpl::CreateDriverEntry(&fake_item); + + EXPECT_CALL(mock_client_, OnDownloadUpdated(DriverEntryEqual(entry))) + .Times(1) + .RetiresOnSaturation(); + static_cast<AllDownloadItemNotifier::Observer*>(driver_.get()) + ->OnDownloadUpdated(&mock_manager_, &fake_item); + + // Nothing happens for cancelled state. + fake_item.SetState(DownloadState::CANCELLED); + static_cast<AllDownloadItemNotifier::Observer*>(driver_.get()) + ->OnDownloadUpdated(&mock_manager_, &fake_item); + + fake_item.SetReceivedBytes(1024); + fake_item.SetState(DownloadState::COMPLETE); + entry = DownloadDriverImpl::CreateDriverEntry(&fake_item); + EXPECT_CALL(mock_client_, OnDownloadSucceeded(DriverEntryEqual(entry))) + .Times(1) + .RetiresOnSaturation(); + static_cast<AllDownloadItemNotifier::Observer*>(driver_.get()) + ->OnDownloadUpdated(&mock_manager_, &fake_item); + + fake_item.SetState(DownloadState::INTERRUPTED); + fake_item.SetLastReason( + DownloadInterruptReason::DOWNLOAD_INTERRUPT_REASON_SERVER_BAD_CONTENT); + entry = DownloadDriverImpl::CreateDriverEntry(&fake_item); + EXPECT_CALL(mock_client_, OnDownloadFailed(DriverEntryEqual(entry), + FailureType::RECOVERABLE)) + .Times(1) + .RetiresOnSaturation(); + static_cast<AllDownloadItemNotifier::Observer*>(driver_.get()) + ->OnDownloadUpdated(&mock_manager_, &fake_item); +} + +TEST_F(DownloadDriverImplTest, TestGetActiveDownloadsCall) { + using DownloadState = content::DownloadItem::DownloadState; + content::FakeDownloadItem item1; + item1.SetState(DownloadState::IN_PROGRESS); + item1.SetGuid(base::GenerateGUID()); + + content::FakeDownloadItem item2; + item2.SetState(DownloadState::CANCELLED); + item2.SetGuid(base::GenerateGUID()); + + content::FakeDownloadItem item3; + item3.SetState(DownloadState::COMPLETE); + item3.SetGuid(base::GenerateGUID()); + + content::FakeDownloadItem item4; + item4.SetState(DownloadState::INTERRUPTED); + item4.SetGuid(base::GenerateGUID()); + + std::vector<content::DownloadItem*> items{&item1, &item2, &item3, &item4}; + + ON_CALL(mock_manager_, GetAllDownloads(_)) + .WillByDefault(PopulateVector(items)); + + EXPECT_CALL(mock_manager_, IsManagerInitialized()) + .Times(1) + .WillOnce(Return(true)); + EXPECT_CALL(mock_client_, OnDriverReady(true)).Times(1); + driver_->Initialize(&mock_client_); + + auto guids = driver_->GetActiveDownloads(); + + EXPECT_EQ(1U, guids.size()); + EXPECT_NE(guids.end(), guids.find(item1.GetGuid())); +} + +} // namespace download diff --git a/chromium/components/download/content/public/BUILD.gn b/chromium/components/download/content/public/BUILD.gn new file mode 100644 index 00000000000..4cb80da314b --- /dev/null +++ b/chromium/components/download/content/public/BUILD.gn @@ -0,0 +1,38 @@ +# 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. + +if (is_android) { + import("//build/config/android/config.gni") + import("//build/config/android/rules.gni") +} + +source_set("public") { + sources = [ + "all_download_item_notifier.cc", + "all_download_item_notifier.h", + ] + + public_deps = [ + "//base", + ] + + deps = [ + "//content/public/browser", + ] +} + +source_set("unit_tests") { + testonly = true + + sources = [ + "all_download_item_notifier_unittest.cc", + ] + + deps = [ + ":public", + "//content/test:test_support", + "//testing/gmock", + "//testing/gtest", + ] +} diff --git a/chromium/components/download/content/public/all_download_item_notifier.cc b/chromium/components/download/content/public/all_download_item_notifier.cc new file mode 100644 index 00000000000..81159f57063 --- /dev/null +++ b/chromium/components/download/content/public/all_download_item_notifier.cc @@ -0,0 +1,75 @@ +// 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/content/public/all_download_item_notifier.h" + +namespace download { + +AllDownloadItemNotifier::AllDownloadItemNotifier( + content::DownloadManager* manager, + AllDownloadItemNotifier::Observer* observer) + : manager_(manager), observer_(observer) { + DCHECK(observer_); + manager_->AddObserver(this); + content::DownloadManager::DownloadVector items; + manager_->GetAllDownloads(&items); + for (content::DownloadManager::DownloadVector::const_iterator it = + items.begin(); + it != items.end(); ++it) { + (*it)->AddObserver(this); + observing_.insert(*it); + } + + if (manager_->IsManagerInitialized()) + observer_->OnManagerInitialized(manager_); +} + +AllDownloadItemNotifier::~AllDownloadItemNotifier() { + if (manager_) + manager_->RemoveObserver(this); + for (std::set<content::DownloadItem*>::const_iterator it = observing_.begin(); + it != observing_.end(); ++it) { + (*it)->RemoveObserver(this); + } + observing_.clear(); +} + +void AllDownloadItemNotifier::OnManagerInitialized() { + observer_->OnManagerInitialized(manager_); +} + +void AllDownloadItemNotifier::ManagerGoingDown( + content::DownloadManager* manager) { + DCHECK_EQ(manager_, manager); + observer_->OnManagerGoingDown(manager); + manager_->RemoveObserver(this); + manager_ = NULL; +} + +void AllDownloadItemNotifier::OnDownloadCreated( + content::DownloadManager* manager, + content::DownloadItem* item) { + item->AddObserver(this); + observing_.insert(item); + observer_->OnDownloadCreated(manager, item); +} + +void AllDownloadItemNotifier::OnDownloadUpdated(content::DownloadItem* item) { + observer_->OnDownloadUpdated(manager_, item); +} + +void AllDownloadItemNotifier::OnDownloadOpened(content::DownloadItem* item) { + observer_->OnDownloadOpened(manager_, item); +} + +void AllDownloadItemNotifier::OnDownloadRemoved(content::DownloadItem* item) { + observer_->OnDownloadRemoved(manager_, item); +} + +void AllDownloadItemNotifier::OnDownloadDestroyed(content::DownloadItem* item) { + item->RemoveObserver(this); + observing_.erase(item); +} + +} // namespace download diff --git a/chromium/components/download/content/public/all_download_item_notifier.h b/chromium/components/download/content/public/all_download_item_notifier.h new file mode 100644 index 00000000000..d52cc92543c --- /dev/null +++ b/chromium/components/download/content/public/all_download_item_notifier.h @@ -0,0 +1,94 @@ +// 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. + +#ifndef COMPONENTS_DOWNLOAD_CONTENT_PUBLIC_ALL_DOWNLOAD_ITEM_NOTIFIER_H_ +#define COMPONENTS_DOWNLOAD_CONTENT_PUBLIC_ALL_DOWNLOAD_ITEM_NOTIFIER_H_ + +#include <set> + +#include "base/macros.h" +#include "content/public/browser/download_item.h" +#include "content/public/browser/download_manager.h" + +// AllDownloadItemNotifier observes ALL the DownloadItems on a given +// DownloadManager. +// Clients should use GetManager() instead of storing their own pointer to the +// manager so that they can be sensitive to managers that have gone down. + +// Example Usage: +// class DownloadSystemConsumer : public AllDownloadItemNotifier::Observer { +// public: +// DownloadSystemConsumer(DownloadManager* original_manager, +// DownloadManager* incognito_manager) +// : original_notifier_(original_manager, this), +// incognito_notifier_(incognito_manager, this) { +// } +// +// virtual void OnDownloadUpdated( +// DownloadManager* manager, DownloadItem* item) { ... } +// +// private: +// AllDownloadItemNotifier original_notifier_; +// AllDownloadItemNotifier incognito_notifier_; +// }; + +namespace download { + +class AllDownloadItemNotifier : public content::DownloadManager::Observer, + public content::DownloadItem::Observer { + public: + // All of the methods take the DownloadManager so that subclasses can observe + // multiple managers at once and easily distinguish which manager a given item + // belongs to. + class Observer { + public: + Observer() {} + virtual ~Observer() {} + + virtual void OnManagerInitialized(content::DownloadManager* manager) {} + virtual void OnManagerGoingDown(content::DownloadManager* manager) {} + virtual void OnDownloadCreated(content::DownloadManager* manager, + content::DownloadItem* item) {} + virtual void OnDownloadUpdated(content::DownloadManager* manager, + content::DownloadItem* item) {} + virtual void OnDownloadOpened(content::DownloadManager* manager, + content::DownloadItem* item) {} + virtual void OnDownloadRemoved(content::DownloadManager* manager, + content::DownloadItem* item) {} + + private: + DISALLOW_COPY_AND_ASSIGN(Observer); + }; + + AllDownloadItemNotifier(content::DownloadManager* manager, + Observer* observer); + + ~AllDownloadItemNotifier() override; + + // Returns NULL if the manager has gone down. + content::DownloadManager* GetManager() const { return manager_; } + + private: + // DownloadManager::Observer + void OnManagerInitialized() override; + void ManagerGoingDown(content::DownloadManager* manager) override; + void OnDownloadCreated(content::DownloadManager* manager, + content::DownloadItem* item) override; + + // DownloadItem::Observer + void OnDownloadUpdated(content::DownloadItem* item) override; + void OnDownloadOpened(content::DownloadItem* item) override; + void OnDownloadRemoved(content::DownloadItem* item) override; + void OnDownloadDestroyed(content::DownloadItem* item) override; + + content::DownloadManager* manager_; + AllDownloadItemNotifier::Observer* observer_; + std::set<content::DownloadItem*> observing_; + + DISALLOW_COPY_AND_ASSIGN(AllDownloadItemNotifier); +}; + +} // namespace download + +#endif // COMPONENTS_DOWNLOAD_CONTENT_PUBLIC_ALL_DOWNLOAD_ITEM_NOTIFIER_H_ diff --git a/chromium/components/download/content/public/all_download_item_notifier_unittest.cc b/chromium/components/download/content/public/all_download_item_notifier_unittest.cc new file mode 100644 index 00000000000..271fed869f7 --- /dev/null +++ b/chromium/components/download/content/public/all_download_item_notifier_unittest.cc @@ -0,0 +1,114 @@ +// 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/content/public/all_download_item_notifier.h" + +#include "base/macros.h" +#include "content/public/test/mock_download_item.h" +#include "content/public/test/mock_download_manager.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using testing::NiceMock; +using testing::SetArgPointee; +using testing::_; + +namespace download { +namespace { + +class MockNotifierObserver : public AllDownloadItemNotifier::Observer { + public: + MockNotifierObserver() {} + virtual ~MockNotifierObserver() {} + + MOCK_METHOD2(OnDownloadCreated, + void(content::DownloadManager* manager, + content::DownloadItem* item)); + MOCK_METHOD2(OnDownloadUpdated, + void(content::DownloadManager* manager, + content::DownloadItem* item)); + MOCK_METHOD2(OnDownloadOpened, + void(content::DownloadManager* manager, + content::DownloadItem* item)); + MOCK_METHOD2(OnDownloadRemoved, + void(content::DownloadManager* manager, + content::DownloadItem* item)); + + private: + DISALLOW_COPY_AND_ASSIGN(MockNotifierObserver); +}; + +class AllDownloadItemNotifierTest : public testing::Test { + public: + AllDownloadItemNotifierTest() + : download_manager_(new content::MockDownloadManager) {} + + virtual ~AllDownloadItemNotifierTest() {} + + content::MockDownloadManager& manager() { return *download_manager_.get(); } + + content::MockDownloadItem& item() { return item_; } + + content::DownloadItem::Observer* NotifierAsItemObserver() const { + return notifier_.get(); + } + + content::DownloadManager::Observer* NotifierAsManagerObserver() const { + return notifier_.get(); + } + + MockNotifierObserver& observer() { return observer_; } + + void SetNotifier() { + EXPECT_CALL(*download_manager_.get(), AddObserver(_)); + notifier_.reset( + new AllDownloadItemNotifier(download_manager_.get(), &observer_)); + } + + void ClearNotifier() { notifier_.reset(); } + + private: + NiceMock<content::MockDownloadItem> item_; + std::unique_ptr<content::MockDownloadManager> download_manager_; + std::unique_ptr<AllDownloadItemNotifier> notifier_; + NiceMock<MockNotifierObserver> observer_; + + DISALLOW_COPY_AND_ASSIGN(AllDownloadItemNotifierTest); +}; + +} // namespace + +TEST_F(AllDownloadItemNotifierTest, AllDownloadItemNotifierTest_0) { + content::DownloadManager::DownloadVector items; + items.push_back(&item()); + EXPECT_CALL(manager(), GetAllDownloads(_)).WillOnce(SetArgPointee<0>(items)); + SetNotifier(); + + EXPECT_CALL(observer(), OnDownloadUpdated(&manager(), &item())); + NotifierAsItemObserver()->OnDownloadUpdated(&item()); + + EXPECT_CALL(observer(), OnDownloadOpened(&manager(), &item())); + NotifierAsItemObserver()->OnDownloadOpened(&item()); + + EXPECT_CALL(observer(), OnDownloadRemoved(&manager(), &item())); + NotifierAsItemObserver()->OnDownloadRemoved(&item()); + + EXPECT_CALL(manager(), RemoveObserver(NotifierAsManagerObserver())); + ClearNotifier(); +} + +TEST_F(AllDownloadItemNotifierTest, AllDownloadItemNotifierTest_1) { + EXPECT_CALL(manager(), GetAllDownloads(_)); + SetNotifier(); + + EXPECT_CALL(observer(), OnDownloadCreated(&manager(), &item())); + NotifierAsManagerObserver()->OnDownloadCreated(&manager(), &item()); + + EXPECT_CALL(manager(), RemoveObserver(NotifierAsManagerObserver())); + NotifierAsManagerObserver()->ManagerGoingDown(&manager()); + + ClearNotifier(); +} + +} // namespace download diff --git a/chromium/components/download/internal/BUILD.gn b/chromium/components/download/internal/BUILD.gn index d1e1e43b885..f138251c6fc 100644 --- a/chromium/components/download/internal/BUILD.gn +++ b/chromium/components/download/internal/BUILD.gn @@ -10,36 +10,59 @@ if (is_android) { static_library("internal") { visibility = [ ":*", - "//components/download", - "//components/download/content", + "//components/download/content/factory", + "//components/download/content/internal", "//components/download/internal/test:test_support", ] sources = [ + "client_set.cc", + "client_set.h", "config.cc", "config.h", + "controller.h", + "controller_impl.cc", + "controller_impl.h", "download_driver.h", "download_service_impl.cc", "download_service_impl.h", + "download_store.cc", + "download_store.h", "driver_entry.cc", "driver_entry.h", "entry.cc", "entry.h", + "entry_utils.cc", + "entry_utils.h", + "file_monitor.h", + "file_monitor_impl.cc", + "file_monitor_impl.h", "model.h", "model_impl.cc", "model_impl.h", - "noop_store.cc", - "noop_store.h", - "scheduler/battery_listener.cc", - "scheduler/battery_listener.h", - "scheduler/network_listener.cc", - "scheduler/network_listener.h", + "proto_conversions.cc", + "proto_conversions.h", + "scheduler/device_status.cc", + "scheduler/device_status.h", + "scheduler/device_status_listener.cc", + "scheduler/device_status_listener.h", + "scheduler/scheduler.h", + "scheduler/scheduler_impl.cc", + "scheduler/scheduler_impl.h", + "service_config_impl.cc", + "service_config_impl.h", + "startup_status.cc", + "startup_status.h", + "stats.cc", + "stats.h", "store.h", ] deps = [ "//base", + "//components/download/internal/proto", "//components/download/public", + "//components/leveldb_proto", "//net", ] } @@ -47,18 +70,26 @@ static_library("internal") { source_set("unit_tests") { testonly = true - visibility = [ "//components/download:unit_tests" ] - sources = [ + "client_set_unittest.cc", + "controller_impl_unittest.cc", + "download_service_impl_unittest.cc", + "download_store_unittest.cc", + "entry_utils_unittest.cc", + "file_monitor_unittest.cc", "model_impl_unittest.cc", - "scheduler/battery_listener_unittest.cc", - "scheduler/network_listener_unittest.cc", + "proto_conversions_unittest.cc", + "scheduler/device_status_listener_unittest.cc", + "scheduler/scheduler_impl_unittest.cc", + "service_config_impl_unittest.cc", ] deps = [ ":internal", "//base/test:test_support", + "//components/download/internal/proto", "//components/download/internal/test:test_support", + "//components/leveldb_proto:test_support", "//testing/gmock", "//testing/gtest", ] diff --git a/chromium/components/download/internal/DEPS b/chromium/components/download/internal/DEPS index 4ca86d9b1fe..6a99578f611 100644 --- a/chromium/components/download/internal/DEPS +++ b/chromium/components/download/internal/DEPS @@ -1,6 +1,7 @@ include_rules = [ "-components/download/content", "-content", + "+components/leveldb_proto", "+base", "+net", ] diff --git a/chromium/components/download/internal/client_set.cc b/chromium/components/download/internal/client_set.cc new file mode 100644 index 00000000000..f74834b0d9a --- /dev/null +++ b/chromium/components/download/internal/client_set.cc @@ -0,0 +1,30 @@ +// 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/client_set.h" + +namespace download { + +ClientSet::ClientSet(std::unique_ptr<DownloadClientMap> clients) + : clients_(std::move(clients)) { + DCHECK(clients_->find(DownloadClient::INVALID) == clients_->end()); +} + +ClientSet::~ClientSet() = default; + +std::set<DownloadClient> ClientSet::GetRegisteredClients() const { + std::set<DownloadClient> clients; + for (const auto& it : *clients_) { + clients.insert(it.first); + } + + return clients; +} + +Client* ClientSet::GetClient(DownloadClient id) const { + const auto& it = clients_->find(id); + return it == clients_->end() ? nullptr : it->second.get(); +} + +} // namespace download diff --git a/chromium/components/download/internal/client_set.h b/chromium/components/download/internal/client_set.h new file mode 100644 index 00000000000..2daa5206782 --- /dev/null +++ b/chromium/components/download/internal/client_set.h @@ -0,0 +1,35 @@ +// 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. + +#ifndef COMPONENTS_DOWNLOAD_INTERNAL_CLIENT_SET_H_ +#define COMPONENTS_DOWNLOAD_INTERNAL_CLIENT_SET_H_ + +#include <map> +#include <memory> +#include <set> + +#include "base/macros.h" +#include "components/download/public/clients.h" + +namespace download { + +// Helper class to hold a list of Clients and associates them with their +// respective DownloadClient identifier. +class ClientSet { + public: + explicit ClientSet(std::unique_ptr<DownloadClientMap> clients); + virtual ~ClientSet(); + + std::set<DownloadClient> GetRegisteredClients() const; + Client* GetClient(DownloadClient id) const; + + private: + std::unique_ptr<DownloadClientMap> clients_; + + DISALLOW_COPY_AND_ASSIGN(ClientSet); +}; + +} // namespace download + +#endif // COMPONENTS_DOWNLOAD_INTERNAL_CLIENT_SET_H_ diff --git a/chromium/components/download/internal/client_set_unittest.cc b/chromium/components/download/internal/client_set_unittest.cc new file mode 100644 index 00000000000..04d454c72e3 --- /dev/null +++ b/chromium/components/download/internal/client_set_unittest.cc @@ -0,0 +1,43 @@ +// 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/client_set.h" + +#include <algorithm> + +#include "base/memory/ptr_util.h" +#include "components/download/internal/test/empty_client.h" +#include "components/download/public/clients.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace download { + +TEST(DownloadServiceClientSetTest, TestGetClient) { + auto client = base::MakeUnique<test::EmptyClient>(); + Client* raw_client = client.get(); + + auto client_map = base::MakeUnique<DownloadClientMap>(); + client_map->insert(std::make_pair(DownloadClient::TEST, std::move(client))); + ClientSet clients(std::move(client_map)); + + EXPECT_EQ(raw_client, clients.GetClient(DownloadClient::TEST)); + EXPECT_EQ(nullptr, clients.GetClient(DownloadClient::INVALID)); +} + +TEST(DownloadServiceClientSetTest, TestGetRegisteredClients) { + auto client = base::MakeUnique<test::EmptyClient>(); + + auto client_map = base::MakeUnique<DownloadClientMap>(); + client_map->insert(std::make_pair(DownloadClient::TEST, std::move(client))); + ClientSet clients(std::move(client_map)); + + std::set<DownloadClient> expected_set = {DownloadClient::TEST}; + std::set<DownloadClient> actual_set = clients.GetRegisteredClients(); + + EXPECT_EQ(expected_set.size(), actual_set.size()); + EXPECT_TRUE( + std::equal(expected_set.begin(), expected_set.end(), actual_set.begin())); +} + +} // namespace download diff --git a/chromium/components/download/internal/config.cc b/chromium/components/download/internal/config.cc index 564da91b881..02bcfdde4ef 100644 --- a/chromium/components/download/internal/config.cc +++ b/chromium/components/download/internal/config.cc @@ -16,29 +16,50 @@ namespace download { namespace { // Default value for max concurrent downloads configuration. -const int kDefaultMaxConcurrentDownloads = 4; +const uint32_t kDefaultMaxConcurrentDownloads = 4; // Default value for maximum running downloads of the download service. -const int kDefaultMaxRunningDownloads = 1; +const uint32_t kDefaultMaxRunningDownloads = 2; // Default value for maximum scheduled downloads. -const int kDefaultMaxScheduledDownloads = 15; +const uint32_t kDefaultMaxScheduledDownloads = 15; // Default value for maximum retry count. -const int kDefaultMaxRetryCount = 5; +const uint32_t kDefaultMaxRetryCount = 5; // Default value for file keep alive time in minutes, keep the file alive for // 12 hours by default. -const int kDefaultFileKeepAliveTimeMinutes = 12 * 60; +const base::TimeDelta kDefaultFileKeepAliveTime = + base::TimeDelta::FromHours(12); + +// Default value for file cleanup window in minutes, the system will schedule a +// cleanup task within this window. +const base::TimeDelta kDefaultFileCleanupWindow = + base::TimeDelta::FromHours(24); + +// Default value for the start window time for OS to schedule background task. +const base::TimeDelta kDefaultWindowStartTime = base::TimeDelta::FromMinutes(5); + +// Default value for the end window time for OS to schedule background task. +const base::TimeDelta kDefaultWindowEndTime = base::TimeDelta::FromHours(8); + +// The default delay to notify the observer when network changes from +// disconnected to connected. +const base::TimeDelta kDefaultNetworkChangeDelay = + base::TimeDelta::FromSeconds(5); + +// The default value of download retry delay when the download is failed. +const base::TimeDelta kDefaultDownloadRetryDelay = + base::TimeDelta::FromSeconds(20); // Helper routine to get Finch experiment parameter. If no Finch seed was found, // use the |default_value|. The |name| should match an experiment // parameter in Finch server configuration. -int GetFinchConfigInt(const std::string& name, int default_value) { +uint32_t GetFinchConfigUInt(const std::string& name, uint32_t default_value) { std::string finch_value = base::GetFieldTrialParamValueByFeature(kDownloadServiceFeature, name); - int result; - return base::StringToInt(finch_value, &result) ? result : default_value; + uint32_t result; + return base::StringToUint(finch_value, &result) ? result : default_value; } } // namespace @@ -46,16 +67,36 @@ int GetFinchConfigInt(const std::string& name, int default_value) { // static std::unique_ptr<Configuration> Configuration::CreateFromFinch() { std::unique_ptr<Configuration> config(new Configuration()); - config->max_concurrent_downloads = GetFinchConfigInt( + config->max_concurrent_downloads = GetFinchConfigUInt( kMaxConcurrentDownloadsConfig, kDefaultMaxConcurrentDownloads); - config->max_running_downloads = GetFinchConfigInt( + config->max_running_downloads = GetFinchConfigUInt( kMaxRunningDownloadsConfig, kDefaultMaxRunningDownloads); - config->max_scheduled_downloads = GetFinchConfigInt( + config->max_scheduled_downloads = GetFinchConfigUInt( kMaxScheduledDownloadsConfig, kDefaultMaxScheduledDownloads); config->max_retry_count = - GetFinchConfigInt(kMaxRetryCountConfig, kDefaultMaxRetryCount); - config->file_keep_alive_time = base::TimeDelta::FromMinutes(GetFinchConfigInt( - kFileKeepAliveTimeMinutesConfig, kDefaultFileKeepAliveTimeMinutes)); + GetFinchConfigUInt(kMaxRetryCountConfig, kDefaultMaxRetryCount); + config->file_keep_alive_time = + base::TimeDelta::FromMinutes(base::saturated_cast<int>( + GetFinchConfigUInt(kFileKeepAliveTimeMinutesConfig, + kDefaultFileKeepAliveTime.InMinutes()))); + config->file_cleanup_window = + base::TimeDelta::FromMinutes(base::saturated_cast<int>( + GetFinchConfigUInt(kFileCleanupWindowMinutesConfig, + kDefaultFileCleanupWindow.InMinutes()))); + config->window_start_time = + base::TimeDelta::FromSeconds(base::saturated_cast<int>(GetFinchConfigUInt( + kWindowStartTimeSecondsConfig, kDefaultWindowStartTime.InSeconds()))); + config->window_end_time = + base::TimeDelta::FromSeconds(base::saturated_cast<int>(GetFinchConfigUInt( + kWindowEndTimeSecondsConfig, kDefaultWindowEndTime.InSeconds()))); + config->network_change_delay = + base::TimeDelta::FromMilliseconds(base::saturated_cast<int>( + GetFinchConfigUInt(kNetworkChangeDelayMsConfig, + kDefaultNetworkChangeDelay.InMilliseconds()))); + config->download_retry_delay = + base::TimeDelta::FromMilliseconds(base::saturated_cast<int>( + GetFinchConfigUInt(kDownloadRetryDelayMsConfig, + kDefaultDownloadRetryDelay.InMilliseconds()))); return config; } @@ -64,7 +105,11 @@ Configuration::Configuration() max_running_downloads(kDefaultMaxRunningDownloads), max_scheduled_downloads(kDefaultMaxScheduledDownloads), max_retry_count(kDefaultMaxRetryCount), - file_keep_alive_time( - base::TimeDelta::FromMinutes(kDefaultFileKeepAliveTimeMinutes)) {} + file_keep_alive_time(kDefaultFileKeepAliveTime), + file_cleanup_window(kDefaultFileCleanupWindow), + window_start_time(kDefaultWindowStartTime), + window_end_time(kDefaultWindowEndTime), + network_change_delay(kDefaultNetworkChangeDelay), + download_retry_delay(kDefaultDownloadRetryDelay) {} } // namespace download diff --git a/chromium/components/download/internal/config.h b/chromium/components/download/internal/config.h index c5b3b0d44d3..073676fcc38 100644 --- a/chromium/components/download/internal/config.h +++ b/chromium/components/download/internal/config.h @@ -27,6 +27,23 @@ constexpr char kMaxRetryCountConfig[] = "max_retry_count"; // Configuration name for file keep alive time. constexpr char kFileKeepAliveTimeMinutesConfig[] = "file_keep_alive_time"; +// Configuration name for file keep alive time. +constexpr char kFileCleanupWindowMinutesConfig[] = "file_cleanup_window"; + +// Configuration name for window start time. +constexpr char kWindowStartTimeSecondsConfig[] = "window_start_time_seconds"; + +// Configuration name for window end time. +constexpr char kWindowEndTimeSecondsConfig[] = "window_end_time_seconds"; + +// Configuration name for the delay to notify network status change, measured in +// milliseconds. +constexpr char kNetworkChangeDelayMsConfig[] = "network_change_delay_ms"; + +// Configuration name for the retry delay when the download is failed, measured +// in milliseconds. +constexpr char kDownloadRetryDelayMsConfig[] = "retry_delay_ms"; + // Download service configuration. // // Loaded based on experiment parameters from the server. Use default values if @@ -39,23 +56,41 @@ struct Configuration { // The maximum number of downloads the DownloadService can have currently in // Active or Paused states. - int max_concurrent_downloads; + uint32_t max_concurrent_downloads; // The maximum number of downloads the DownloadService can have currently in // only Active state. - int max_running_downloads; + uint32_t max_running_downloads; - // The maximum number of downloads that are scheduled but not yet in Active - // state, for each client using the download service. - int max_scheduled_downloads; + // The maximum number of downloads that are scheduled for each client using + // the download service. + uint32_t max_scheduled_downloads; // The maximum number of retries before the download is aborted. - int max_retry_count; + uint32_t max_retry_count; // The time that the download service will keep the files around before // deleting them if the client hasn't handle the files. base::TimeDelta file_keep_alive_time; + // The length of the flexible time window during which the scheduler must + // schedule a file cleanup task. + base::TimeDelta file_cleanup_window; + + // The start window time in seconds for OS to schedule background task. + // The OS will trigger the background task in this window. + base::TimeDelta window_start_time; + + // The end window time in seconds for OS to schedule background task. + // The OS will trigger the background task in this window. + base::TimeDelta window_end_time; + + // The delay to notify network status changes. + base::TimeDelta network_change_delay; + + // The delay to retry a download when the download is failed. + base::TimeDelta download_retry_delay; + private: DISALLOW_COPY_AND_ASSIGN(Configuration); }; diff --git a/chromium/components/download/internal/controller.h b/chromium/components/download/internal/controller.h new file mode 100644 index 00000000000..a19a223f0f5 --- /dev/null +++ b/chromium/components/download/internal/controller.h @@ -0,0 +1,110 @@ +// 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. + +#ifndef COMPONENTS_DOWNLOAD_INTERNAL_CONTROLLER_H_ +#define COMPONENTS_DOWNLOAD_INTERNAL_CONTROLLER_H_ + +#include <string> + +#include "base/macros.h" +#include "components/download/public/clients.h" +#include "components/download/public/download_service.h" +#include "components/download/public/download_task_types.h" + +namespace download { + +struct DownloadParams; +struct SchedulingParams; + +// The type of completion when the download entry transits to complete state. +// TODO(xingliu): Implement timeout and unknown failure types. +enum class CompletionType { + // The download is successfully finished. + SUCCEED = 0, + // The download is interrupted and failed. + FAIL = 1, + // The download is aborted by the client. + ABORT = 2, + // The download is timed out and the connection is closed. + TIMEOUT = 3, + // The download is failed for unknown reasons. + UNKNOWN = 4, + // The download is cancelled by the client. + CANCEL = 5, + // The count of entries for the enum. + COUNT = 6, +}; + +// The core Controller responsible for gluing various DownloadService components +// together to manage the active downloads. +class Controller { + public: + enum class State { + // The Controller has been created but has not been initialized yet. It + // cannot be used. + CREATED = 1, + + // The Controller has been created and Initialize() has been called but has + // not yet finished. It cannot be used. + INITIALIZING = 2, + + // The Controller has been created and initialized. It can be used. + READY = 3, + + // The Controller failed to initialize and is in the process of recovering. + // It cannot be used. + RECOVERING = 4, + + // The Controller was unable to recover and is unusable this session. + UNAVAILABLE = 5, + }; + + Controller() = default; + virtual ~Controller() = default; + + // Initializes the controller. Initialization may be asynchronous. + virtual void Initialize(const base::Closure& callback) = 0; + + // Returns the status of Controller. + virtual State GetState() = 0; + + // Starts a download with |params|. See DownloadParams::StartCallback and + // DownloadParams::StartResponse for information on how a caller can determine + // whether or not the download was successfully accepted and queued. + virtual void StartDownload(const DownloadParams& params) = 0; + + // Pauses a download request associated with |guid| if one exists. + virtual void PauseDownload(const std::string& guid) = 0; + + // Resumes a download request associated with |guid| if one exists. The + // download request may or may not start downloading at this time, but it will + // no longer be blocked by any prior PauseDownload() actions. + virtual void ResumeDownload(const std::string& guid) = 0; + + // Cancels a download request associated with |guid| if one exists. + virtual void CancelDownload(const std::string& guid) = 0; + + // Changes the SchedulingParams of a download request associated with |guid| + // to |params|. + virtual void ChangeDownloadCriteria(const std::string& guid, + const SchedulingParams& params) = 0; + + // Exposes the owner of the download request for |guid| if one exists. + // Otherwise returns DownloadClient::INVALID for an unowned entry. + virtual DownloadClient GetOwnerOfDownload(const std::string& guid) = 0; + + // See DownloadService::OnStartScheduledTask. + virtual void OnStartScheduledTask(DownloadTaskType task_type, + const TaskFinishedCallback& callback) = 0; + + // See DownloadService::OnStopScheduledTask. + virtual bool OnStopScheduledTask(DownloadTaskType task_type) = 0; + + private: + DISALLOW_COPY_AND_ASSIGN(Controller); +}; + +} // namespace download + +#endif // COMPONENTS_DOWNLOAD_INTERNAL_CONTROLLER_H_ diff --git a/chromium/components/download/internal/controller_impl.cc b/chromium/components/download/internal/controller_impl.cc new file mode 100644 index 00000000000..03fa70c46ec --- /dev/null +++ b/chromium/components/download/internal/controller_impl.cc @@ -0,0 +1,1034 @@ +// 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)), + controller_state_(State::CREATED), + weak_ptr_factory_(this) {} + +ControllerImpl::~ControllerImpl() = default; + +void ControllerImpl::Initialize(const base::Closure& callback) { + DCHECK_EQ(controller_state_, State::CREATED); + + init_callback_ = callback; + controller_state_ = State::INITIALIZING; + + driver_->Initialize(this); + model_->Initialize(this); + file_monitor_->Initialize(base::Bind(&ControllerImpl::OnFileMonitorReady, + weak_ptr_factory_.GetWeakPtr())); +} + +Controller::State ControllerImpl::GetState() { + return controller_state_; +} + +void ControllerImpl::StartDownload(const DownloadParams& params) { + DCHECK(controller_state_ == State::READY || + controller_state_ == State::UNAVAILABLE); + + // TODO(dtrainor): Validate all input parameters. + DCHECK_LE(base::Time::Now(), params.scheduling_params.cancel_time); + + if (controller_state_ != State::READY) { + HandleStartDownloadResponse(params.client, params.guid, + DownloadParams::StartResult::INTERNAL_ERROR, + params.callback); + return; + } + + KillTimedOutDownloads(); + + 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(controller_state_ == State::READY || + controller_state_ == State::UNAVAILABLE); + if (controller_state_ != State::READY) + 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(controller_state_ == State::READY || + controller_state_ == State::UNAVAILABLE); + if (controller_state_ != State::READY) + 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(controller_state_ == State::READY || + controller_state_ == State::UNAVAILABLE); + if (controller_state_ != State::READY) + 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(controller_state_ == State::READY || + controller_state_ == State::UNAVAILABLE); + if (controller_state_ != State::READY) + 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(controller_state_ == State::READY || + controller_state_ == State::UNAVAILABLE); + if (controller_state_ != State::READY) + return DownloadClient::INVALID; + + auto* entry = model_->Get(guid); + return entry ? entry->client : DownloadClient::INVALID; +} + +void ControllerImpl::OnStartScheduledTask( + DownloadTaskType task_type, + const TaskFinishedCallback& callback) { + task_finished_callbacks_[task_type] = callback; + + switch (controller_state_) { + case State::READY: + if (task_type == DownloadTaskType::DOWNLOAD_TASK) { + ActivateMoreDownloads(); + } else if (task_type == DownloadTaskType::CLEANUP_TASK) { + RemoveCleanupEligibleDownloads(); + ScheduleCleanupTask(); + } + break; + case State::UNAVAILABLE: + HandleTaskFinished(task_type, false, + stats::ScheduledTaskStatus::ABORTED_ON_FAILED_INIT); + break; + case State::CREATED: // Intentional fallthrough. + case State::INITIALIZING: // Intentional fallthrough. + case State::RECOVERING: // Intentional fallthrough. + default: + NOTREACHED(); + break; + } +} + +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::OnDriverHardRecoverComplete(bool success) { + DCHECK(!startup_status_.driver_ok.has_value()); + startup_status_.driver_ok = success; + AttemptToFinalizeSetup(); +} + +void ControllerImpl::OnDownloadCreated(const DriverEntry& download) { + if (controller_state_ != State::READY) + 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); + stats::LogStartDownloadResponse(entry->client, should_download); + if (should_download == ShouldDownload::ABORT) { + HandleCompleteDownload(CompletionType::ABORT, entry->guid); + } +} + +void ControllerImpl::OnDownloadFailed(const DriverEntry& download, + FailureType failure_type) { + if (controller_state_ != State::READY) + return; + + Entry* entry = model_->Get(download.guid); + if (!entry) { + HandleExternalDownload(download.guid, false); + return; + } + + if (failure_type == FailureType::RECOVERABLE) { + // Because the network offline signal comes later than actual download + // failure, retry the download after a delay to avoid the retry to fail + // immediately again. + base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( + FROM_HERE, + base::Bind(&ControllerImpl::UpdateDriverStateWithGuid, + weak_ptr_factory_.GetWeakPtr(), download.guid), + config_->download_retry_delay); + } 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 (controller_state_ != State::READY) + 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 (controller_state_ != State::READY) + 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::OnFileMonitorHardRecoverComplete(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::OnModelHardRecoverComplete(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) { + if (controller_state_ != State::READY) + return; + + UpdateDriverStates(); + ActivateMoreDownloads(); +} + +void ControllerImpl::AttemptToFinalizeSetup() { + DCHECK(controller_state_ == State::INITIALIZING || + controller_state_ == State::RECOVERING); + + if (!startup_status_.Complete()) + return; + + bool in_recovery = controller_state_ == State::RECOVERING; + + stats::LogControllerStartupStatus(in_recovery, startup_status_); + if (!startup_status_.Ok()) { + if (in_recovery) { + HandleUnrecoverableSetup(); + NotifyServiceOfStartup(); + } else { + StartHardRecoveryAttempt(); + } + + return; + } + + device_status_listener_->Start(this); + PollActiveDriverDownloads(); + CancelOrphanedRequests(); + CleanupUnknownFiles(); + RemoveCleanupEligibleDownloads(); + ResolveInitialRequestStates(); + + NotifyClientsOfStartup(in_recovery); + + controller_state_ = State::READY; + + UpdateDriverStates(); + + KillTimedOutDownloads(); + NotifyServiceOfStartup(); + + // Pull the initial straw if active downloads haven't reach maximum. + ActivateMoreDownloads(); +} + +void ControllerImpl::HandleUnrecoverableSetup() { + controller_state_ = State::UNAVAILABLE; + + // 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())); +} + +void ControllerImpl::StartHardRecoveryAttempt() { + startup_status_.Reset(); + controller_state_ = State::RECOVERING; + + driver_->HardRecover(); + model_->HardRecover(); + file_monitor_->HardRecover( + base::Bind(&ControllerImpl::OnFileMonitorHardRecoverComplete, + weak_ptr_factory_.GetWeakPtr())); +} + +void ControllerImpl::PollActiveDriverDownloads() { + DCHECK(controller_state_ == State::INITIALIZING || + controller_state_ == State::RECOVERING); + + std::set<std::string> guids = driver_->GetActiveDownloads(); + + for (auto guid : guids) { + if (!model_->Get(guid)) + externally_active_downloads_.insert(guid); + } +} + +void ControllerImpl::CancelOrphanedRequests() { + DCHECK(controller_state_ == State::INITIALIZING || + controller_state_ == State::RECOVERING); + + 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() { + DCHECK(controller_state_ == State::INITIALIZING || + controller_state_ == State::RECOVERING); + + 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() { + DCHECK(controller_state_ == State::INITIALIZING || + controller_state_ == State::RECOVERING); + + 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; + case Entry::State::COUNT: + NOTREACHED(); + break; + } + } +} + +void ControllerImpl::UpdateDriverStates() { + DCHECK(startup_status_.Complete()); + + for (auto* entry : model_->PeekEntries()) + UpdateDriverState(entry); +} + +void ControllerImpl::UpdateDriverStateWithGuid(const std::string& guid) { + Entry* entry = model_->Get(guid); + if (entry) + UpdateDriverState(entry); +} + +void ControllerImpl::UpdateDriverState(Entry* entry) { + DCHECK_EQ(controller_state_, State::READY); + + 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(bool state_lost) { + 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, + state_lost, 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); + + if (entry->state == Entry::State::COMPLETE) { + DVLOG(1) << "Download is already completed."; + return; + } + + auto driver_entry = driver_->Find(guid); + uint64_t file_size = + driver_entry.has_value() ? driver_entry->bytes_downloaded : 0; + stats::LogDownloadCompletion( + type, entry->completion_time - entry->create_time, file_size); + + if (type == CompletionType::SUCCEED) { + DCHECK(driver_entry.has_value()); + stats::LogFilePathRenamed(driver_entry->current_file_path != + entry->target_file_path); + 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, + std::ceil(start_time.InSecondsF()), + std::ceil(end_time.InSecondsF())); +} + +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 (controller_state_ != State::READY) + 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, + bool state_lost, + const std::vector<std::string>& guids) { + auto* client = clients_->GetClient(client_id); + DCHECK(client); + client->OnServiceInitialized(state_lost, 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 diff --git a/chromium/components/download/internal/controller_impl.h b/chromium/components/download/internal/controller_impl.h new file mode 100644 index 00000000000..911ada78f00 --- /dev/null +++ b/chromium/components/download/internal/controller_impl.h @@ -0,0 +1,234 @@ +// 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. + +#ifndef COMPONENTS_DOWNLOAD_INTERNAL_CONTROLLER_IMPL_H_ +#define COMPONENTS_DOWNLOAD_INTERNAL_CONTROLLER_IMPL_H_ + +#include <map> +#include <memory> +#include <set> + +#include "base/cancelable_callback.h" +#include "base/macros.h" +#include "base/memory/weak_ptr.h" +#include "base/optional.h" +#include "components/download/internal/controller.h" +#include "components/download/internal/download_driver.h" +#include "components/download/internal/entry.h" +#include "components/download/internal/model.h" +#include "components/download/internal/scheduler/device_status_listener.h" +#include "components/download/internal/startup_status.h" +#include "components/download/internal/stats.h" +#include "components/download/public/client.h" +#include "components/download/public/download_params.h" +#include "components/download/public/task_scheduler.h" + +namespace download { + +class ClientSet; +class DownloadDriver; +class FileMonitor; +class Model; +class Scheduler; + +struct Configuration; +struct SchedulingParams; + +// The internal Controller implementation. This class does all of the heavy +// lifting for the DownloadService. +class ControllerImpl : public Controller, + public DownloadDriver::Client, + public Model::Client, + public DeviceStatusListener::Observer { + public: + // |config| is externally owned and must be guaranteed to outlive this class. + 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); + ~ControllerImpl() override; + + // Controller implementation. + void Initialize(const base::Closure& callback) override; + State GetState() override; + void StartDownload(const DownloadParams& params) override; + void PauseDownload(const std::string& guid) override; + void ResumeDownload(const std::string& guid) override; + void CancelDownload(const std::string& guid) override; + void ChangeDownloadCriteria(const std::string& guid, + const SchedulingParams& params) override; + DownloadClient GetOwnerOfDownload(const std::string& guid) override; + void OnStartScheduledTask(DownloadTaskType task_type, + const TaskFinishedCallback& callback) override; + bool OnStopScheduledTask(DownloadTaskType task_type) override; + + private: + // DownloadDriver::Client implementation. + void OnDriverReady(bool success) override; + void OnDriverHardRecoverComplete(bool success) override; + void OnDownloadCreated(const DriverEntry& download) override; + void OnDownloadFailed(const DriverEntry& download, + FailureType failure_type) override; + void OnDownloadSucceeded(const DriverEntry& download) override; + void OnDownloadUpdated(const DriverEntry& download) override; + + // Model::Client implementation. + void OnModelReady(bool success) override; + void OnModelHardRecoverComplete(bool success) override; + void OnItemAdded(bool success, + DownloadClient client, + const std::string& guid) override; + void OnItemUpdated(bool success, + DownloadClient client, + const std::string& guid) override; + void OnItemRemoved(bool success, + DownloadClient client, + const std::string& guid) override; + + // Called when the file monitor and download file directory are initialized. + void OnFileMonitorReady(bool success); + + // Called when the file monitor finishes attempting to recover itself. + void OnFileMonitorHardRecoverComplete(bool success); + + // DeviceStatusListener::Observer implementation. + void OnDeviceStatusChanged(const DeviceStatus& device_status) override; + + // Checks if initialization is complete and successful. If so, completes the + // internal state initialization. + void AttemptToFinalizeSetup(); + + // Called when setup and recovery failed. Shuts down the service and notifies + // the Clients. + void HandleUnrecoverableSetup(); + + // If initialization failed, try to reset the state of all components and + // restart them. If that attempt fails the service will be unavailable. + void StartHardRecoveryAttempt(); + + // Checks for all the currently active driver downloads. This lets us know + // which ones are active that we haven't tracked. + void PollActiveDriverDownloads(); + + // Cancels and cleans upany requests that are no longer associated with a + // Client in |clients_|. + void CancelOrphanedRequests(); + + // Cleans up any files that are left on the disk without any entries. + void CleanupUnknownFiles(); + + // Fixes any discrepancies in state between |model_| and |driver_|. Meant to + // resolve state issues during startup. + void ResolveInitialRequestStates(); + + // Updates the driver states based on the states of entries in download + // service. + void UpdateDriverStates(); + + // See |UpdateDriverState|. + void UpdateDriverStateWithGuid(const std::string& guid); + + // Processes the download based on the state of |entry|. May start, pause + // or resume a download accordingly. + void UpdateDriverState(Entry* entry); + + // Notifies all Client in |clients_| that this controller is initialized and + // lets them know which download requests we are aware of for their + // DownloadClient. + void NotifyClientsOfStartup(bool state_lost); + + // Notifies the service that the startup has completed so that it can start + // processing any pending requests. + void NotifyServiceOfStartup(); + + void HandleStartDownloadResponse(DownloadClient client, + const std::string& guid, + DownloadParams::StartResult result); + void HandleStartDownloadResponse( + DownloadClient client, + const std::string& guid, + DownloadParams::StartResult result, + const DownloadParams::StartCallback& callback); + + // Handles and clears any pending task finished callbacks. + void HandleTaskFinished(DownloadTaskType task_type, + bool needs_reschedule, + stats::ScheduledTaskStatus status); + void OnCompleteCleanupTask(); + + void HandleCompleteDownload(CompletionType type, const std::string& guid); + + // Find more available entries to download, until the number of active entries + // reached maximum. + void ActivateMoreDownloads(); + + void RemoveCleanupEligibleDownloads(); + + void HandleExternalDownload(const std::string& guid, bool active); + + // Postable methods meant to just be pass throughs to Client APIs. This is + // meant to help prevent reentrancy. + void SendOnServiceInitialized(DownloadClient client_id, + bool state_lost, + const std::vector<std::string>& guids); + void SendOnServiceUnavailable(); + void SendOnDownloadUpdated(DownloadClient client_id, + const std::string& guid, + uint64_t bytes_downloaded); + void SendOnDownloadSucceeded(DownloadClient client_id, + const std::string& guid, + const base::FilePath& path, + uint64_t size); + void SendOnDownloadFailed(DownloadClient client_id, + const std::string& guid, + download::Client::FailureReason reason); + + // Schedules a cleanup task in future based on status of entries. + void ScheduleCleanupTask(); + + // Posts a task to cancel the downloads in future based on the cancel_after + // time of the entries. If cancel time for an entry is already surpassed, the + // task will be posted right away which will clean the entry. + void ScheduleKillDownloadTaskIfNecessary(); + + // Kills the downloads which have surpassed their cancel_after time. + void KillTimedOutDownloads(); + + Configuration* config_; + + // The directory in which the downloaded files are stored. + const base::FilePath download_file_dir_; + + // Owned Dependencies. + 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_; + + // Internal state. + base::Closure init_callback_; + State controller_state_; + StartupStatus startup_status_; + std::set<std::string> externally_active_downloads_; + std::map<std::string, DownloadParams::StartCallback> start_callbacks_; + std::map<DownloadTaskType, TaskFinishedCallback> task_finished_callbacks_; + base::CancelableClosure cancel_downloads_callback_; + + // Only used to post tasks on the same thread. + base::WeakPtrFactory<ControllerImpl> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(ControllerImpl); +}; + +} // namespace download + +#endif // COMPONENTS_DOWNLOAD_INTERNAL_CONTROLLER_IMPL_H_ diff --git a/chromium/components/download/internal/controller_impl_unittest.cc b/chromium/components/download/internal/controller_impl_unittest.cc new file mode 100644 index 00000000000..5a5a7a1bbb7 --- /dev/null +++ b/chromium/components/download/internal/controller_impl_unittest.cc @@ -0,0 +1,1405 @@ +// 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 <algorithm> +#include <memory> + +#include "base/bind.h" +#include "base/guid.h" +#include "base/macros.h" +#include "base/memory/ptr_util.h" +#include "base/strings/string_util.h" +#include "base/test/histogram_tester.h" +#include "base/test/test_simple_task_runner.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/file_monitor.h" +#include "components/download/internal/model_impl.h" +#include "components/download/internal/scheduler/scheduler.h" +#include "components/download/internal/stats.h" +#include "components/download/internal/test/entry_utils.h" +#include "components/download/internal/test/mock_client.h" +#include "components/download/internal/test/test_device_status_listener.h" +#include "components/download/internal/test/test_download_driver.h" +#include "components/download/internal/test/test_store.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using testing::_; +using testing::NiceMock; +using testing::Return; +using testing::SaveArg; + +namespace download { + +namespace { + +const base::FilePath::CharType kDownloadDirPath[] = + FILE_PATH_LITERAL("/test/downloads"); + +bool GuidInEntryList(const std::vector<Entry>& entries, + const std::string& guid) { + for (const auto& entry : entries) { + if (entry.guid == guid) + return true; + } + return false; +} + +DriverEntry BuildDriverEntry(const Entry& entry, DriverEntry::State state) { + DriverEntry dentry; + dentry.guid = entry.guid; + dentry.state = state; + return dentry; +} + +void NotifyTaskFinished(bool success) {} + +class MockTaskScheduler : public TaskScheduler { + public: + MockTaskScheduler() = default; + ~MockTaskScheduler() override = default; + + // TaskScheduler implementation. + MOCK_METHOD5(ScheduleTask, void(DownloadTaskType, bool, bool, long, long)); + MOCK_METHOD1(CancelTask, void(DownloadTaskType)); +}; + +class MockScheduler : public Scheduler { + public: + MockScheduler() = default; + ~MockScheduler() override = default; + + MOCK_METHOD1(Reschedule, void(const Model::EntryList&)); + MOCK_METHOD2(Next, Entry*(const Model::EntryList&, const DeviceStatus&)); +}; + +class MockFileMonitor : public FileMonitor { + public: + MockFileMonitor() = default; + ~MockFileMonitor() override = default; + + void TriggerInit(bool success); + void TriggerHardRecover(bool success); + + void Initialize(const FileMonitor::InitCallback& callback) override; + MOCK_METHOD2(DeleteUnknownFiles, + void(const Model::EntryList&, const std::vector<DriverEntry>&)); + MOCK_METHOD2(CleanupFilesForCompletedEntries, + std::vector<Entry*>(const Model::EntryList&, + const base::Closure&)); + MOCK_METHOD2(DeleteFiles, + void(const std::set<base::FilePath>&, stats::FileCleanupReason)); + void HardRecover(const FileMonitor::InitCallback&) override; + + private: + FileMonitor::InitCallback init_callback_; + FileMonitor::InitCallback recover_callback_; +}; + +void MockFileMonitor::TriggerInit(bool success) { + init_callback_.Run(success); +} + +void MockFileMonitor::TriggerHardRecover(bool success) { + recover_callback_.Run(success); +} + +void MockFileMonitor::Initialize(const FileMonitor::InitCallback& callback) { + init_callback_ = callback; +} + +void MockFileMonitor::HardRecover(const FileMonitor::InitCallback& callback) { + recover_callback_ = callback; +} + +class DownloadServiceControllerImplTest : public testing::Test { + public: + DownloadServiceControllerImplTest() + : task_runner_(new base::TestSimpleTaskRunner), + handle_(task_runner_), + controller_(nullptr), + client_(nullptr), + driver_(nullptr), + store_(nullptr), + model_(nullptr), + device_status_listener_(nullptr), + scheduler_(nullptr), + file_monitor_(nullptr), + init_callback_called_(false) { + start_callback_ = + base::Bind(&DownloadServiceControllerImplTest::StartCallback, + base::Unretained(this)); + } + + ~DownloadServiceControllerImplTest() override = default; + + void SetUp() override { + auto client = base::MakeUnique<test::MockClient>(); + auto driver = base::MakeUnique<test::TestDownloadDriver>(); + auto store = base::MakeUnique<test::TestStore>(); + config_ = base::MakeUnique<Configuration>(); + config_->max_retry_count = 2; + config_->file_keep_alive_time = base::TimeDelta::FromMinutes(10); + config_->file_cleanup_window = base::TimeDelta::FromMinutes(5); + config_->max_concurrent_downloads = 5; + config_->max_running_downloads = 5; + + client_ = client.get(); + driver_ = driver.get(); + store_ = store.get(); + + auto clients = base::MakeUnique<DownloadClientMap>(); + clients->insert(std::make_pair(DownloadClient::TEST, std::move(client))); + auto client_set = base::MakeUnique<ClientSet>(std::move(clients)); + auto model = base::MakeUnique<ModelImpl>(std::move(store)); + auto device_status_listener = + base::MakeUnique<test::TestDeviceStatusListener>(); + auto scheduler = base::MakeUnique<NiceMock<MockScheduler>>(); + auto task_scheduler = base::MakeUnique<MockTaskScheduler>(); + + auto download_file_dir = base::FilePath(kDownloadDirPath); + auto file_monitor = base::MakeUnique<MockFileMonitor>(); + + model_ = model.get(); + device_status_listener_ = device_status_listener.get(); + scheduler_ = scheduler.get(); + task_scheduler_ = task_scheduler.get(); + file_monitor_ = file_monitor.get(); + + controller_ = base::MakeUnique<ControllerImpl>( + config_.get(), std::move(client_set), std::move(driver), + std::move(model), std::move(device_status_listener), + std::move(scheduler), std::move(task_scheduler), + std::move(file_monitor), download_file_dir); + } + + protected: + void OnInitCompleted() { + EXPECT_TRUE(controller_->GetState() == Controller::State::READY || + controller_->GetState() == Controller::State::UNAVAILABLE); + init_callback_called_ = true; + } + + void InitializeController() { + controller_->Initialize( + base::Bind(&DownloadServiceControllerImplTest::OnInitCompleted, + base::Unretained(this))); + } + + DownloadParams MakeDownloadParams() { + DownloadParams params; + params.client = DownloadClient::TEST; + params.guid = base::GenerateGUID(); + params.callback = start_callback_; + return params; + } + + MOCK_METHOD2(StartCallback, + void(const std::string&, DownloadParams::StartResult)); + + scoped_refptr<base::TestSimpleTaskRunner> task_runner_; + base::ThreadTaskRunnerHandle handle_; + + std::unique_ptr<ControllerImpl> controller_; + std::unique_ptr<Configuration> config_; + test::MockClient* client_; + test::TestDownloadDriver* driver_; + test::TestStore* store_; + ModelImpl* model_; + test::TestDeviceStatusListener* device_status_listener_; + MockScheduler* scheduler_; + MockTaskScheduler* task_scheduler_; + MockFileMonitor* file_monitor_; + + DownloadParams::StartCallback start_callback_; + bool init_callback_called_; + + private: + DISALLOW_COPY_AND_ASSIGN(DownloadServiceControllerImplTest); +}; + +} // namespace + +TEST_F(DownloadServiceControllerImplTest, SuccessfulInitModelFirst) { + base::HistogramTester histogram_tester; + EXPECT_CALL(*client_, OnServiceInitialized(false, _)).Times(0); + EXPECT_EQ(controller_->GetState(), Controller::State::CREATED); + + InitializeController(); + EXPECT_TRUE(store_->init_called()); + EXPECT_EQ(controller_->GetState(), Controller::State::INITIALIZING); + + store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>()); + file_monitor_->TriggerInit(true); + EXPECT_EQ(controller_->GetState(), Controller::State::INITIALIZING); + + EXPECT_CALL(*client_, OnServiceInitialized(false, _)).Times(1); + EXPECT_CALL(*scheduler_, Next(_, _)).Times(1); + EXPECT_CALL(*scheduler_, Reschedule(_)).Times(1); + + driver_->MakeReady(); + EXPECT_EQ(controller_->GetState(), Controller::State::READY); + + task_runner_->RunUntilIdle(); + + histogram_tester.ExpectBucketCount( + "Download.Service.StartUpStatus.Initialization", + static_cast<base::HistogramBase::Sample>(stats::StartUpResult::SUCCESS), + 1); +} + +TEST_F(DownloadServiceControllerImplTest, SuccessfulInitDriverFirst) { + base::HistogramTester histogram_tester; + EXPECT_CALL(*client_, OnServiceInitialized(false, _)).Times(0); + EXPECT_EQ(controller_->GetState(), Controller::State::CREATED); + + InitializeController(); + EXPECT_TRUE(store_->init_called()); + EXPECT_EQ(controller_->GetState(), Controller::State::INITIALIZING); + + driver_->MakeReady(); + EXPECT_FALSE(init_callback_called_); + EXPECT_EQ(controller_->GetState(), Controller::State::INITIALIZING); + + EXPECT_CALL(*client_, OnServiceInitialized(false, _)).Times(1); + EXPECT_CALL(*scheduler_, Next(_, _)).Times(1); + EXPECT_CALL(*scheduler_, Reschedule(_)).Times(1); + + store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>()); + file_monitor_->TriggerInit(true); + EXPECT_EQ(controller_->GetState(), Controller::State::READY); + + task_runner_->RunUntilIdle(); + EXPECT_TRUE(init_callback_called_); + + histogram_tester.ExpectBucketCount( + "Download.Service.StartUpStatus.Initialization", + static_cast<base::HistogramBase::Sample>(stats::StartUpResult::SUCCESS), + 1); +} + +TEST_F(DownloadServiceControllerImplTest, HardRecoveryAfterFailedModel) { + base::HistogramTester histogram_tester; + EXPECT_CALL(*client_, OnServiceInitialized(true, _)).Times(0); + EXPECT_EQ(controller_->GetState(), Controller::State::CREATED); + + InitializeController(); + driver_->MakeReady(); + store_->TriggerInit(false, base::MakeUnique<std::vector<Entry>>()); + file_monitor_->TriggerInit(true); + + EXPECT_EQ(controller_->GetState(), Controller::State::RECOVERING); + driver_->TriggerHardRecoverComplete(true); + store_->TriggerHardRecover(true); + file_monitor_->TriggerHardRecover(true); + + EXPECT_CALL(*client_, OnServiceInitialized(true, _)).Times(1); + task_runner_->RunUntilIdle(); + histogram_tester.ExpectBucketCount( + "Download.Service.StartUpStatus.Initialization", + static_cast<base::HistogramBase::Sample>(stats::StartUpResult::FAILURE), + 1); + histogram_tester.ExpectBucketCount( + "Download.Service.StartUpStatus.Initialization", + static_cast<base::HistogramBase::Sample>( + stats::StartUpResult::FAILURE_REASON_MODEL), + 1); + histogram_tester.ExpectBucketCount( + "Download.Service.StartUpStatus.Recovery", + static_cast<base::HistogramBase::Sample>(stats::StartUpResult::SUCCESS), + 1); +} + +TEST_F(DownloadServiceControllerImplTest, HardRecoveryAfterFailedFileMonitor) { + base::HistogramTester histogram_tester; + EXPECT_CALL(*client_, OnServiceInitialized(true, _)).Times(0); + EXPECT_EQ(controller_->GetState(), Controller::State::CREATED); + + InitializeController(); + driver_->MakeReady(); + store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>()); + file_monitor_->TriggerInit(false); + + EXPECT_EQ(controller_->GetState(), Controller::State::RECOVERING); + driver_->TriggerHardRecoverComplete(true); + store_->TriggerHardRecover(true); + file_monitor_->TriggerHardRecover(true); + + EXPECT_CALL(*client_, OnServiceInitialized(true, _)).Times(1); + task_runner_->RunUntilIdle(); + histogram_tester.ExpectBucketCount( + "Download.Service.StartUpStatus.Initialization", + static_cast<base::HistogramBase::Sample>(stats::StartUpResult::FAILURE), + 1); + histogram_tester.ExpectBucketCount( + "Download.Service.StartUpStatus.Initialization", + static_cast<base::HistogramBase::Sample>( + stats::StartUpResult::FAILURE_REASON_FILE_MONITOR), + 1); + histogram_tester.ExpectBucketCount( + "Download.Service.StartUpStatus.Recovery", + static_cast<base::HistogramBase::Sample>(stats::StartUpResult::SUCCESS), + 1); +} + +TEST_F(DownloadServiceControllerImplTest, HardRecoveryFails) { + base::HistogramTester histogram_tester; + EXPECT_CALL(*client_, OnServiceInitialized(true, _)).Times(0); + EXPECT_EQ(controller_->GetState(), Controller::State::CREATED); + + InitializeController(); + driver_->MakeReady(); + store_->TriggerInit(false, base::MakeUnique<std::vector<Entry>>()); + file_monitor_->TriggerInit(true); + + EXPECT_EQ(controller_->GetState(), Controller::State::RECOVERING); + driver_->TriggerHardRecoverComplete(true); + store_->TriggerHardRecover(true); + file_monitor_->TriggerHardRecover(false); + + EXPECT_CALL(*client_, OnServiceUnavailable()).Times(1); + task_runner_->RunUntilIdle(); + histogram_tester.ExpectBucketCount( + "Download.Service.StartUpStatus.Initialization", + static_cast<base::HistogramBase::Sample>(stats::StartUpResult::FAILURE), + 1); + histogram_tester.ExpectBucketCount( + "Download.Service.StartUpStatus.Initialization", + static_cast<base::HistogramBase::Sample>( + stats::StartUpResult::FAILURE_REASON_MODEL), + 1); + histogram_tester.ExpectBucketCount( + "Download.Service.StartUpStatus.Recovery", + static_cast<base::HistogramBase::Sample>(stats::StartUpResult::FAILURE), + 1); + histogram_tester.ExpectBucketCount( + "Download.Service.StartUpStatus.Recovery", + static_cast<base::HistogramBase::Sample>( + stats::StartUpResult::FAILURE_REASON_FILE_MONITOR), + 1); +} + +TEST_F(DownloadServiceControllerImplTest, SuccessfulInitWithExistingDownload) { + Entry entry1 = test::BuildBasicEntry(); + Entry entry2 = test::BuildBasicEntry(); + Entry entry3 = + test::BuildEntry(DownloadClient::INVALID, base::GenerateGUID()); + + std::vector<Entry> entries = {entry1, entry2, entry3}; + std::vector<std::string> expected_guids = {entry1.guid, entry2.guid}; + + EXPECT_CALL(*client_, + OnServiceInitialized( + false, testing::UnorderedElementsAreArray(expected_guids))); + EXPECT_CALL(*scheduler_, Next(_, _)).Times(1); + EXPECT_CALL(*scheduler_, Reschedule(_)).Times(1); + + InitializeController(); + driver_->MakeReady(); + store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>(entries)); + file_monitor_->TriggerInit(true); + + task_runner_->RunUntilIdle(); + EXPECT_TRUE(init_callback_called_); +} + +TEST_F(DownloadServiceControllerImplTest, UnknownFileDeletion) { + Entry entry1 = test::BuildBasicEntry(); + Entry entry2 = test::BuildBasicEntry(); + Entry entry3 = test::BuildBasicEntry(); + + std::vector<Entry> entries = {entry1, entry2, entry3}; + + DriverEntry dentry1 = + BuildDriverEntry(entry1, DriverEntry::State::IN_PROGRESS); + DriverEntry dentry3 = + BuildDriverEntry(entry3, DriverEntry::State::IN_PROGRESS); + std::vector<DriverEntry> dentries = {dentry1, dentry3}; + + EXPECT_CALL(*scheduler_, Next(_, _)).Times(1); + EXPECT_CALL(*scheduler_, Reschedule(_)).Times(1); + EXPECT_CALL(*file_monitor_, DeleteUnknownFiles(_, _)).Times(1); + + driver_->AddTestData(dentries); + InitializeController(); + driver_->MakeReady(); + store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>(entries)); + file_monitor_->TriggerInit(true); + + task_runner_->RunUntilIdle(); +} + +TEST_F(DownloadServiceControllerImplTest, + CleanupTaskCallsFileMonitorAndSchedulesNewTaskInFuture) { + Entry entry1 = test::BuildBasicEntry(); + Entry entry2 = test::BuildBasicEntry(); + Entry entry3 = test::BuildBasicEntry(); + entry3.state = Entry::State::COMPLETE; + entry3.completion_time = base::Time::Now(); + + std::vector<Entry> entries = {entry1, entry2, entry3}; + + EXPECT_CALL(*file_monitor_, CleanupFilesForCompletedEntries(_, _)).Times(2); + EXPECT_CALL(*task_scheduler_, + ScheduleTask(DownloadTaskType::CLEANUP_TASK, _, _, _, _)) + .Times(1); + + InitializeController(); + driver_->MakeReady(); + store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>(entries)); + file_monitor_->TriggerInit(true); + controller_->OnStartScheduledTask(DownloadTaskType::CLEANUP_TASK, + base::Bind(&NotifyTaskFinished)); + + task_runner_->RunUntilIdle(); +} + +TEST_F(DownloadServiceControllerImplTest, GetOwnerOfDownload) { + Entry entry = test::BuildBasicEntry(); + std::vector<Entry> entries = {entry}; + + EXPECT_CALL(*client_, OnServiceInitialized(false, _)).Times(1); + EXPECT_CALL(*scheduler_, Next(_, _)).Times(1); + EXPECT_CALL(*scheduler_, Reschedule(_)).Times(1); + + InitializeController(); + driver_->MakeReady(); + store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>(entries)); + file_monitor_->TriggerInit(true); + + task_runner_->RunUntilIdle(); + + EXPECT_EQ(DownloadClient::TEST, controller_->GetOwnerOfDownload(entry.guid)); + EXPECT_EQ(DownloadClient::INVALID, + controller_->GetOwnerOfDownload(base::GenerateGUID())); +} + +TEST_F(DownloadServiceControllerImplTest, AddDownloadAccepted) { + EXPECT_CALL(*client_, OnServiceInitialized(false, _)).Times(1); + EXPECT_CALL(*scheduler_, Next(_, _)).Times(1); + EXPECT_CALL(*scheduler_, Reschedule(_)).Times(1); + + // Set up the Controller. + InitializeController(); + store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>()); + file_monitor_->TriggerInit(true); + driver_->MakeReady(); + task_runner_->RunUntilIdle(); + + // Trigger the download. + DownloadParams params = MakeDownloadParams(); + EXPECT_CALL(*this, + StartCallback(params.guid, DownloadParams::StartResult::ACCEPTED)) + .Times(1); + EXPECT_CALL(*scheduler_, Next(_, _)).Times(1); + EXPECT_CALL(*scheduler_, Reschedule(_)).Times(1); + controller_->StartDownload(params); + + // TODO(dtrainor): Compare the full DownloadParams with the full Entry. + store_->TriggerUpdate(true); + + std::vector<Entry> entries = store_->updated_entries(); + Entry entry = entries[0]; + DCHECK_EQ(entry.client, DownloadClient::TEST); + EXPECT_TRUE(base::StartsWith(entry.target_file_path.value(), kDownloadDirPath, + base::CompareCase::SENSITIVE)); + + task_runner_->RunUntilIdle(); +} + +TEST_F(DownloadServiceControllerImplTest, AddDownloadFailsWithBackoff) { + EXPECT_CALL(*client_, OnServiceInitialized(false, _)).Times(1); + EXPECT_CALL(*scheduler_, Next(_, _)).Times(1); + EXPECT_CALL(*scheduler_, Reschedule(_)).Times(1); + + Entry entry = test::BuildBasicEntry(); + std::vector<Entry> entries = {entry}; + + // Set up the Controller. + InitializeController(); + store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>(entries)); + file_monitor_->TriggerInit(true); + driver_->MakeReady(); + task_runner_->RunUntilIdle(); + + // Set the failure expectations. + config_->max_scheduled_downloads = 1U; + + // Trigger the download. + DownloadParams params = MakeDownloadParams(); + EXPECT_CALL(*this, + StartCallback(params.guid, DownloadParams::StartResult::BACKOFF)) + .Times(1); + controller_->StartDownload(params); + + EXPECT_FALSE(GuidInEntryList(store_->updated_entries(), params.guid)); + + task_runner_->RunUntilIdle(); +} + +TEST_F(DownloadServiceControllerImplTest, + AddDownloadFailsWithDuplicateGuidInModel) { + EXPECT_CALL(*client_, OnServiceInitialized(false, _)).Times(1); + EXPECT_CALL(*scheduler_, Next(_, _)).Times(1); + EXPECT_CALL(*scheduler_, Reschedule(_)).Times(1); + + Entry entry = test::BuildBasicEntry(); + std::vector<Entry> entries = {entry}; + + // Set up the Controller. + InitializeController(); + store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>(entries)); + file_monitor_->TriggerInit(true); + driver_->MakeReady(); + task_runner_->RunUntilIdle(); + + // Trigger the download. + DownloadParams params = MakeDownloadParams(); + params.guid = entry.guid; + EXPECT_CALL( + *this, + StartCallback(params.guid, DownloadParams::StartResult::UNEXPECTED_GUID)) + .Times(1); + controller_->StartDownload(params); + + task_runner_->RunUntilIdle(); +} + +TEST_F(DownloadServiceControllerImplTest, AddDownloadFailsWithDuplicateCall) { + testing::InSequence sequence; + + EXPECT_CALL(*scheduler_, Next(_, _)).Times(1); + EXPECT_CALL(*scheduler_, Reschedule(_)).Times(1); + EXPECT_CALL(*client_, OnServiceInitialized(false, _)).Times(1); + + // Set up the Controller. + InitializeController(); + store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>()); + file_monitor_->TriggerInit(true); + driver_->MakeReady(); + task_runner_->RunUntilIdle(); + + EXPECT_CALL(*scheduler_, Next(_, _)).Times(1); + EXPECT_CALL(*scheduler_, Reschedule(_)).Times(1); + + // Trigger the download twice. + DownloadParams params = MakeDownloadParams(); + EXPECT_CALL( + *this, + StartCallback(params.guid, DownloadParams::StartResult::UNEXPECTED_GUID)) + .Times(1); + EXPECT_CALL(*this, + StartCallback(params.guid, DownloadParams::StartResult::ACCEPTED)) + .Times(1); + controller_->StartDownload(params); + controller_->StartDownload(params); + store_->TriggerUpdate(true); + + EXPECT_TRUE(GuidInEntryList(store_->updated_entries(), params.guid)); + + task_runner_->RunUntilIdle(); +} + +TEST_F(DownloadServiceControllerImplTest, AddDownloadFailsWithBadClient) { + EXPECT_CALL(*client_, OnServiceInitialized(false, _)).Times(1); + EXPECT_CALL(*scheduler_, Next(_, _)).Times(1); + EXPECT_CALL(*scheduler_, Reschedule(_)).Times(1); + + // Set up the Controller. + InitializeController(); + store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>()); + file_monitor_->TriggerInit(true); + driver_->MakeReady(); + task_runner_->RunUntilIdle(); + + // Trigger the download. + DownloadParams params = MakeDownloadParams(); + params.client = DownloadClient::INVALID; + EXPECT_CALL(*this, + StartCallback(params.guid, + DownloadParams::StartResult::UNEXPECTED_CLIENT)) + .Times(1); + controller_->StartDownload(params); + + task_runner_->RunUntilIdle(); +} + +TEST_F(DownloadServiceControllerImplTest, AddDownloadFailsWithClientCancel) { + EXPECT_CALL(*client_, OnServiceInitialized(false, _)).Times(1); + EXPECT_CALL(*scheduler_, Next(_, _)).Times(1); + EXPECT_CALL(*scheduler_, Reschedule(_)).Times(1); + + // Set up the Controller. + InitializeController(); + store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>()); + file_monitor_->TriggerInit(true); + driver_->MakeReady(); + task_runner_->RunUntilIdle(); + + // Trigger the download. + DownloadParams params = MakeDownloadParams(); + EXPECT_CALL( + *this, + StartCallback(params.guid, DownloadParams::StartResult::CLIENT_CANCELLED)) + .Times(1); + controller_->StartDownload(params); + + controller_->CancelDownload(params.guid); + store_->TriggerUpdate(true); + + task_runner_->RunUntilIdle(); +} + +TEST_F(DownloadServiceControllerImplTest, AddDownloadFailsWithInternalError) { + EXPECT_CALL(*client_, OnServiceInitialized(false, _)).Times(1); + EXPECT_CALL(*scheduler_, Next(_, _)).Times(1); + EXPECT_CALL(*scheduler_, Reschedule(_)).Times(1); + + // Set up the Controller. + InitializeController(); + store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>()); + file_monitor_->TriggerInit(true); + driver_->MakeReady(); + task_runner_->RunUntilIdle(); + + // Trigger the download. + DownloadParams params = MakeDownloadParams(); + EXPECT_CALL(*this, StartCallback(params.guid, + DownloadParams::StartResult::INTERNAL_ERROR)) + .Times(1); + controller_->StartDownload(params); + + store_->TriggerUpdate(false); + + task_runner_->RunUntilIdle(); +} + +TEST_F(DownloadServiceControllerImplTest, Pause) { + // Setup download service test data. + Entry entry1 = test::BuildBasicEntry(); + Entry entry2 = test::BuildBasicEntry(); + Entry entry3 = test::BuildBasicEntry(); + entry1.state = Entry::State::AVAILABLE; + entry2.state = Entry::State::ACTIVE; + entry3.state = Entry::State::COMPLETE; + std::vector<Entry> entries = {entry1, entry2, entry3}; + + EXPECT_CALL(*client_, OnServiceInitialized(false, _)).Times(1); + EXPECT_CALL(*scheduler_, Next(_, _)).Times(3); + EXPECT_CALL(*scheduler_, Reschedule(_)).Times(3); + + // Set the network status to disconnected so no entries will be polled from + // the scheduler. + InitializeController(); + store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>(entries)); + file_monitor_->TriggerInit(true); + driver_->MakeReady(); + + // Setup download driver test data. + DriverEntry driver_entry1, driver_entry2, driver_entry3; + driver_entry1.guid = entry1.guid; + driver_entry1.state = DriverEntry::State::IN_PROGRESS; + driver_entry2.guid = entry2.guid; + driver_entry2.state = DriverEntry::State::IN_PROGRESS; + driver_entry3.guid = entry3.guid; + driver_->AddTestData( + std::vector<DriverEntry>{driver_entry1, driver_entry2, driver_entry3}); + + // Pause in progress available entry. + EXPECT_EQ(Entry::State::AVAILABLE, model_->Get(entry1.guid)->state); + controller_->PauseDownload(entry1.guid); + EXPECT_TRUE(driver_->Find(entry1.guid)->paused); + EXPECT_EQ(Entry::State::PAUSED, model_->Get(entry1.guid)->state); + + // Pause in progress active entry. + controller_->PauseDownload(entry2.guid); + EXPECT_TRUE(driver_->Find(entry2.guid)->paused); + EXPECT_EQ(Entry::State::PAUSED, model_->Get(entry2.guid)->state); + + // Entries in complete states can't be paused. + controller_->PauseDownload(entry3.guid); + EXPECT_FALSE(driver_->Find(entry3.guid)->paused); + EXPECT_EQ(Entry::State::COMPLETE, model_->Get(entry3.guid)->state); + + task_runner_->RunUntilIdle(); +} + +TEST_F(DownloadServiceControllerImplTest, Resume) { + // Setup download service test data. + Entry entry1 = test::BuildBasicEntry(); + Entry entry2 = test::BuildBasicEntry(); + entry1.state = Entry::State::PAUSED; + entry2.state = Entry::State::ACTIVE; + std::vector<Entry> entries = {entry1, entry2}; + + EXPECT_CALL(*client_, OnServiceInitialized(false, _)).Times(1); + EXPECT_CALL(*scheduler_, Next(_, _)).Times(2); + EXPECT_CALL(*scheduler_, Reschedule(_)).Times(2); + + InitializeController(); + store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>(entries)); + file_monitor_->TriggerInit(true); + driver_->MakeReady(); + + // Setup download driver test data. + DriverEntry driver_entry1, driver_entry2; + driver_entry1.guid = entry1.guid; + driver_entry1.paused = true; + driver_entry2.guid = entry2.guid; + driver_entry2.paused = false; + driver_->AddTestData(std::vector<DriverEntry>{driver_entry1, driver_entry2}); + + // Resume the paused download. + device_status_listener_->SetDeviceStatus( + DeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED)); + EXPECT_EQ(Entry::State::PAUSED, model_->Get(entry1.guid)->state); + controller_->ResumeDownload(entry1.guid); + EXPECT_FALSE(driver_->Find(entry1.guid)->paused); + EXPECT_EQ(Entry::State::ACTIVE, model_->Get(entry1.guid)->state); + + // Entries in paused state can't be resumed. + controller_->ResumeDownload(entry2.guid); + EXPECT_EQ(Entry::State::ACTIVE, model_->Get(entry2.guid)->state); + EXPECT_FALSE(driver_->Find(entry2.guid)->paused); + + task_runner_->RunUntilIdle(); +} + +TEST_F(DownloadServiceControllerImplTest, Cancel) { + Entry entry = test::BuildBasicEntry(); + entry.state = Entry::State::ACTIVE; + std::vector<Entry> entries = {entry}; + + EXPECT_CALL(*client_, OnServiceInitialized(false, _)).Times(1); + EXPECT_CALL(*client_, + OnDownloadFailed(entry.guid, Client::FailureReason::CANCELLED)) + .Times(1); + EXPECT_CALL(*scheduler_, Next(_, _)).Times(2); + EXPECT_CALL(*scheduler_, Reschedule(_)).Times(2); + + InitializeController(); + store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>(entries)); + file_monitor_->TriggerInit(true); + driver_->MakeReady(); + + DriverEntry driver_entry; + driver_entry.guid = entry.guid; + driver_->AddTestData(std::vector<DriverEntry>{driver_entry}); + + controller_->CancelDownload(entry.guid); + EXPECT_EQ(nullptr, model_->Get(entry.guid)); + + task_runner_->RunUntilIdle(); +} + +TEST_F(DownloadServiceControllerImplTest, OnDownloadFailed) { + Entry entry = test::BuildBasicEntry(); + entry.state = Entry::State::ACTIVE; + std::vector<Entry> entries = {entry}; + + EXPECT_CALL(*client_, OnServiceInitialized(false, _)).Times(1); + EXPECT_CALL(*client_, + OnDownloadFailed(entry.guid, Client::FailureReason::NETWORK)) + .Times(1); + EXPECT_CALL(*scheduler_, Next(_, _)).Times(2); + EXPECT_CALL(*scheduler_, Reschedule(_)).Times(2); + + InitializeController(); + store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>(entries)); + file_monitor_->TriggerInit(true); + driver_->MakeReady(); + + DriverEntry driver_entry; + driver_entry.guid = entry.guid; + + driver_->NotifyDownloadFailed(driver_entry, FailureType::NOT_RECOVERABLE); + EXPECT_EQ(nullptr, model_->Get(entry.guid)); + + task_runner_->RunUntilIdle(); +} + +TEST_F(DownloadServiceControllerImplTest, RetryOnFailure) { + Entry entry1 = test::BuildBasicEntry(Entry::State::ACTIVE); + Entry entry2 = test::BuildBasicEntry(Entry::State::ACTIVE); + std::vector<Entry> entries = {entry1, entry2}; + + DriverEntry dentry1 = + BuildDriverEntry(entry1, DriverEntry::State::INTERRUPTED); + DriverEntry dentry2 = + BuildDriverEntry(entry2, DriverEntry::State::INTERRUPTED); + std::vector<DriverEntry> dentries = {dentry1, dentry2}; + + EXPECT_CALL(*client_, OnServiceInitialized(false, _)).Times(1); + + // Set up the Controller. + device_status_listener_->SetDeviceStatus( + DeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED)); + + driver_->AddTestData(dentries); + InitializeController(); + store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>(entries)); + file_monitor_->TriggerInit(true); + driver_->MakeReady(); + task_runner_->RunUntilIdle(); + + // Test retry on failure. + config_->max_retry_count = 4; + EXPECT_CALL(*client_, OnDownloadSucceeded(entry1.guid, _, _)).Times(1); + base::FilePath path = base::FilePath::FromUTF8Unsafe("123"); + driver_->NotifyDownloadFailed(dentry1, FailureType::RECOVERABLE); + driver_->NotifyDownloadFailed(dentry1, FailureType::RECOVERABLE); + driver_->NotifyDownloadSucceeded(dentry1); + + EXPECT_CALL(*client_, + OnDownloadFailed(entry2.guid, Client::FailureReason::NETWORK)) + .Times(1); + driver_->NotifyDownloadFailed(dentry2, FailureType::RECOVERABLE); + driver_->NotifyDownloadFailed(dentry2, FailureType::RECOVERABLE); + driver_->NotifyDownloadFailed(dentry2, FailureType::RECOVERABLE); + driver_->NotifyDownloadFailed(dentry2, FailureType::RECOVERABLE); + // Failed entry should exist because we retry after a delay. + EXPECT_NE(nullptr, model_->Get(entry2.guid)); + + task_runner_->RunUntilIdle(); + // Retry is done, and failed entry should be removed. + EXPECT_EQ(nullptr, model_->Get(entry2.guid)); +} + +TEST_F(DownloadServiceControllerImplTest, OnDownloadSucceeded) { + Entry entry = test::BuildBasicEntry(); + entry.state = Entry::State::ACTIVE; + std::vector<Entry> entries = {entry}; + + EXPECT_CALL(*client_, OnServiceInitialized(false, _)).Times(1); + EXPECT_CALL(*client_, OnDownloadSucceeded(entry.guid, _, _)).Times(1); + EXPECT_CALL(*scheduler_, Next(_, _)).Times(2); + EXPECT_CALL(*scheduler_, Reschedule(_)).Times(2); + + InitializeController(); + store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>(entries)); + file_monitor_->TriggerInit(true); + driver_->MakeReady(); + + DriverEntry driver_entry; + driver_entry.guid = entry.guid; + driver_entry.bytes_downloaded = 1024; + driver_entry.completion_time = base::Time::Now(); + driver_entry.current_file_path = base::FilePath::FromUTF8Unsafe("123"); + + long start_time = 0; + EXPECT_CALL(*task_scheduler_, + ScheduleTask(DownloadTaskType::CLEANUP_TASK, _, _, _, _)) + .WillOnce(SaveArg<3>(&start_time)); + driver_->NotifyDownloadSucceeded(driver_entry); + EXPECT_EQ(Entry::State::COMPLETE, model_->Get(entry.guid)->state); + EXPECT_LE(driver_entry.completion_time + config_->file_keep_alive_time, + base::Time::Now() + base::TimeDelta::FromSeconds(start_time)); + + task_runner_->RunUntilIdle(); +} + +TEST_F(DownloadServiceControllerImplTest, CleanupTaskScheduledAtEarliestTime) { + Entry entry1 = test::BuildBasicEntry(); + entry1.state = Entry::State::ACTIVE; + entry1.completion_time = base::Time::Now() - base::TimeDelta::FromMinutes(1); + Entry entry2 = test::BuildBasicEntry(); + entry2.state = Entry::State::COMPLETE; + entry2.completion_time = base::Time::Now() - base::TimeDelta::FromMinutes(2); + std::vector<Entry> entries = {entry1, entry2}; + + InitializeController(); + store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>(entries)); + file_monitor_->TriggerInit(true); + driver_->MakeReady(); + + DriverEntry driver_entry; + driver_entry.guid = entry1.guid; + driver_entry.bytes_downloaded = 1024; + driver_entry.completion_time = base::Time::Now(); + driver_entry.current_file_path = base::FilePath::FromUTF8Unsafe("123"); + + EXPECT_CALL(*task_scheduler_, ScheduleTask(DownloadTaskType::CLEANUP_TASK, + false, false, 480, 780)) + .Times(1); + driver_->NotifyDownloadSucceeded(driver_entry); + EXPECT_EQ(Entry::State::COMPLETE, model_->Get(entry1.guid)->state); + + task_runner_->RunUntilIdle(); +} + +TEST_F(DownloadServiceControllerImplTest, OnDownloadUpdated) { + Entry entry = test::BuildBasicEntry(); + entry.state = Entry::State::ACTIVE; + std::vector<Entry> entries = {entry}; + + EXPECT_CALL(*client_, OnServiceInitialized(false, _)).Times(1); + EXPECT_CALL(*scheduler_, Next(_, _)).Times(1); + EXPECT_CALL(*scheduler_, Reschedule(_)).Times(1); + + InitializeController(); + store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>(entries)); + file_monitor_->TriggerInit(true); + driver_->MakeReady(); + + DriverEntry driver_entry; + driver_entry.state = DriverEntry::State::IN_PROGRESS; + driver_entry.guid = entry.guid; + driver_entry.bytes_downloaded = 1024; + + EXPECT_CALL(*client_, + OnDownloadUpdated(entry.guid, driver_entry.bytes_downloaded)); + driver_->NotifyDownloadUpdate(driver_entry); + EXPECT_EQ(Entry::State::ACTIVE, model_->Get(entry.guid)->state); + + task_runner_->RunUntilIdle(); +} + +TEST_F(DownloadServiceControllerImplTest, DownloadCompletionTest) { + // TODO(dtrainor): Simulate a UNKNOWN once that is supported. + + Entry entry1 = test::BuildBasicEntry(Entry::State::ACTIVE); + Entry entry2 = test::BuildBasicEntry(Entry::State::ACTIVE); + Entry entry3 = test::BuildBasicEntry(Entry::State::ACTIVE); + Entry entry4 = test::BuildBasicEntry(Entry::State::ACTIVE); + entry4.scheduling_params.cancel_time = base::Time::Now(); + + DriverEntry dentry1 = + BuildDriverEntry(entry1, DriverEntry::State::IN_PROGRESS); + // dentry2 will effectively be created by the test to simulate a start + // download. + DriverEntry dentry3 = + BuildDriverEntry(entry3, DriverEntry::State::IN_PROGRESS); + + std::vector<Entry> entries = {entry1, entry2, entry3, entry4}; + std::vector<DriverEntry> dentries = {dentry1, dentry3}; + + EXPECT_CALL(*client_, OnServiceInitialized(false, _)).Times(1); + + // Test FailureReason::TIMEDOUT. + EXPECT_CALL(*client_, + OnDownloadFailed(entry4.guid, Client::FailureReason::TIMEDOUT)) + .Times(1); + + // Set up the Controller. + driver_->AddTestData(dentries); + InitializeController(); + store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>(entries)); + file_monitor_->TriggerInit(true); + driver_->MakeReady(); + task_runner_->RunUntilIdle(); + + // Test FailureReason::CANCELLED. + EXPECT_CALL(*client_, + OnDownloadFailed(entry1.guid, Client::FailureReason::CANCELLED)) + .Times(1); + controller_->CancelDownload(entry1.guid); + + // Test FailureReason::ABORTED. + EXPECT_CALL(*client_, OnDownloadStarted(entry2.guid, _, _)) + .Times(1) + .WillOnce(Return(Client::ShouldDownload::ABORT)); + EXPECT_CALL(*client_, + OnDownloadFailed(entry2.guid, Client::FailureReason::ABORTED)) + .Times(1); + driver_->Start(RequestParams(), entry2.guid, entry2.target_file_path, + NO_TRAFFIC_ANNOTATION_YET); + + // Test FailureReason::NETWORK. + EXPECT_CALL(*client_, + OnDownloadFailed(entry3.guid, Client::FailureReason::NETWORK)) + .Times(1); + driver_->NotifyDownloadFailed(dentry3, FailureType::NOT_RECOVERABLE); + + task_runner_->RunUntilIdle(); +} + +TEST_F(DownloadServiceControllerImplTest, StartupRecovery) { + EXPECT_CALL(*client_, OnServiceInitialized(false, _)).Times(1); + + std::vector<Entry> entries; + std::vector<DriverEntry> driver_entries; + entries.push_back(test::BuildBasicEntry(Entry::State::NEW)); + driver_entries.push_back( + BuildDriverEntry(entries.back(), DriverEntry::State::IN_PROGRESS)); + entries.push_back(test::BuildBasicEntry(Entry::State::NEW)); + driver_entries.push_back( + BuildDriverEntry(entries.back(), DriverEntry::State::COMPLETE)); + entries.push_back(test::BuildBasicEntry(Entry::State::NEW)); + driver_entries.push_back( + BuildDriverEntry(entries.back(), DriverEntry::State::CANCELLED)); + entries.push_back(test::BuildBasicEntry(Entry::State::NEW)); + driver_entries.push_back( + BuildDriverEntry(entries.back(), DriverEntry::State::INTERRUPTED)); + entries.push_back(test::BuildBasicEntry(Entry::State::NEW)); + + entries.push_back(test::BuildBasicEntry(Entry::State::AVAILABLE)); + driver_entries.push_back( + BuildDriverEntry(entries.back(), DriverEntry::State::IN_PROGRESS)); + entries.push_back(test::BuildBasicEntry(Entry::State::AVAILABLE)); + driver_entries.push_back( + BuildDriverEntry(entries.back(), DriverEntry::State::COMPLETE)); + entries.push_back(test::BuildBasicEntry(Entry::State::AVAILABLE)); + driver_entries.push_back( + BuildDriverEntry(entries.back(), DriverEntry::State::CANCELLED)); + entries.push_back(test::BuildBasicEntry(Entry::State::AVAILABLE)); + driver_entries.push_back( + BuildDriverEntry(entries.back(), DriverEntry::State::INTERRUPTED)); + entries.push_back(test::BuildBasicEntry(Entry::State::AVAILABLE)); + + entries.push_back(test::BuildBasicEntry(Entry::State::ACTIVE)); + driver_entries.push_back( + BuildDriverEntry(entries.back(), DriverEntry::State::IN_PROGRESS)); + entries.push_back(test::BuildBasicEntry(Entry::State::ACTIVE)); + driver_entries.push_back( + BuildDriverEntry(entries.back(), DriverEntry::State::COMPLETE)); + entries.push_back(test::BuildBasicEntry(Entry::State::ACTIVE)); + driver_entries.push_back( + BuildDriverEntry(entries.back(), DriverEntry::State::CANCELLED)); + entries.push_back(test::BuildBasicEntry(Entry::State::ACTIVE)); + driver_entries.push_back( + BuildDriverEntry(entries.back(), DriverEntry::State::INTERRUPTED)); + entries.push_back(test::BuildBasicEntry(Entry::State::ACTIVE)); + + entries.push_back(test::BuildBasicEntry(Entry::State::PAUSED)); + driver_entries.push_back( + BuildDriverEntry(entries.back(), DriverEntry::State::IN_PROGRESS)); + entries.push_back(test::BuildBasicEntry(Entry::State::PAUSED)); + driver_entries.push_back( + BuildDriverEntry(entries.back(), DriverEntry::State::COMPLETE)); + entries.push_back(test::BuildBasicEntry(Entry::State::PAUSED)); + driver_entries.push_back( + BuildDriverEntry(entries.back(), DriverEntry::State::CANCELLED)); + entries.push_back(test::BuildBasicEntry(Entry::State::PAUSED)); + driver_entries.push_back( + BuildDriverEntry(entries.back(), DriverEntry::State::INTERRUPTED)); + entries.push_back(test::BuildBasicEntry(Entry::State::PAUSED)); + + entries.push_back(test::BuildBasicEntry(Entry::State::COMPLETE)); + driver_entries.push_back( + BuildDriverEntry(entries.back(), DriverEntry::State::IN_PROGRESS)); + entries.push_back(test::BuildBasicEntry(Entry::State::COMPLETE)); + driver_entries.push_back( + BuildDriverEntry(entries.back(), DriverEntry::State::COMPLETE)); + entries.push_back(test::BuildBasicEntry(Entry::State::COMPLETE)); + driver_entries.push_back( + BuildDriverEntry(entries.back(), DriverEntry::State::CANCELLED)); + entries.push_back(test::BuildBasicEntry(Entry::State::COMPLETE)); + driver_entries.push_back( + BuildDriverEntry(entries.back(), DriverEntry::State::INTERRUPTED)); + entries.push_back(test::BuildBasicEntry(Entry::State::COMPLETE)); + + // Set up the Controller. + device_status_listener_->SetDeviceStatus( + DeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED)); + + InitializeController(); + driver_->AddTestData(driver_entries); + driver_->MakeReady(); + store_->AutomaticallyTriggerAllFutureCallbacks(true); + store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>(entries)); + file_monitor_->TriggerInit(true); + + // Allow the initialization routines and persistent layers to do their thing. + task_runner_->RunUntilIdle(); + + // Validate Model and DownloadDriver states. + // Note that we are accessing the Model instead of the Store here to make it + // easier to query the states. + // TODO(dtrainor): Check more of the DriverEntry state to validate that the + // entries are either paused or resumed accordingly. + + // Entry::State::NEW. + EXPECT_EQ(Entry::State::AVAILABLE, model_->Get(entries[0].guid)->state); + EXPECT_EQ(Entry::State::AVAILABLE, model_->Get(entries[1].guid)->state); + EXPECT_EQ(Entry::State::AVAILABLE, model_->Get(entries[2].guid)->state); + EXPECT_EQ(Entry::State::AVAILABLE, model_->Get(entries[3].guid)->state); + EXPECT_EQ(Entry::State::AVAILABLE, model_->Get(entries[4].guid)->state); + EXPECT_EQ(base::nullopt, driver_->Find(entries[0].guid)); + EXPECT_EQ(base::nullopt, driver_->Find(entries[1].guid)); + EXPECT_EQ(base::nullopt, driver_->Find(entries[2].guid)); + EXPECT_EQ(base::nullopt, driver_->Find(entries[3].guid)); + EXPECT_EQ(base::nullopt, driver_->Find(entries[4].guid)); + + // Entry::State::AVAILABLE. + EXPECT_EQ(Entry::State::ACTIVE, model_->Get(entries[5].guid)->state); + EXPECT_EQ(Entry::State::COMPLETE, model_->Get(entries[6].guid)->state); + EXPECT_EQ(Entry::State::COMPLETE, model_->Get(entries[7].guid)->state); + EXPECT_EQ(Entry::State::ACTIVE, model_->Get(entries[8].guid)->state); + EXPECT_EQ(Entry::State::AVAILABLE, model_->Get(entries[9].guid)->state); + EXPECT_NE(base::nullopt, driver_->Find(entries[5].guid)); + EXPECT_EQ(base::nullopt, driver_->Find(entries[6].guid)); + EXPECT_EQ(base::nullopt, driver_->Find(entries[7].guid)); + EXPECT_NE(base::nullopt, driver_->Find(entries[8].guid)); + EXPECT_EQ(base::nullopt, driver_->Find(entries[9].guid)); + + // Entry::State::ACTIVE. + EXPECT_EQ(Entry::State::ACTIVE, model_->Get(entries[10].guid)->state); + EXPECT_EQ(Entry::State::COMPLETE, model_->Get(entries[11].guid)->state); + EXPECT_EQ(Entry::State::COMPLETE, model_->Get(entries[12].guid)->state); + EXPECT_EQ(Entry::State::ACTIVE, model_->Get(entries[13].guid)->state); + EXPECT_EQ(Entry::State::ACTIVE, model_->Get(entries[14].guid)->state); + EXPECT_NE(base::nullopt, driver_->Find(entries[10].guid)); + EXPECT_EQ(base::nullopt, driver_->Find(entries[11].guid)); + EXPECT_EQ(base::nullopt, driver_->Find(entries[12].guid)); + EXPECT_NE(base::nullopt, driver_->Find(entries[13].guid)); + EXPECT_NE(base::nullopt, driver_->Find(entries[14].guid)); + + // Entry::State::PAUSED. + EXPECT_EQ(Entry::State::PAUSED, model_->Get(entries[15].guid)->state); + EXPECT_EQ(Entry::State::COMPLETE, model_->Get(entries[16].guid)->state); + EXPECT_EQ(Entry::State::COMPLETE, model_->Get(entries[17].guid)->state); + EXPECT_EQ(Entry::State::PAUSED, model_->Get(entries[18].guid)->state); + EXPECT_EQ(Entry::State::PAUSED, model_->Get(entries[19].guid)->state); + EXPECT_NE(base::nullopt, driver_->Find(entries[15].guid)); + EXPECT_EQ(base::nullopt, driver_->Find(entries[16].guid)); + EXPECT_EQ(base::nullopt, driver_->Find(entries[17].guid)); + EXPECT_NE(base::nullopt, driver_->Find(entries[18].guid)); + EXPECT_EQ(base::nullopt, driver_->Find(entries[19].guid)); + + // prog, comp, canc, int, __ + // Entry::State::COMPLETE. + EXPECT_EQ(Entry::State::COMPLETE, model_->Get(entries[20].guid)->state); + EXPECT_EQ(Entry::State::COMPLETE, model_->Get(entries[21].guid)->state); + EXPECT_EQ(Entry::State::COMPLETE, model_->Get(entries[22].guid)->state); + EXPECT_EQ(Entry::State::COMPLETE, model_->Get(entries[23].guid)->state); + EXPECT_EQ(Entry::State::COMPLETE, model_->Get(entries[24].guid)->state); + EXPECT_EQ(base::nullopt, driver_->Find(entries[20].guid)); + EXPECT_EQ(base::nullopt, driver_->Find(entries[21].guid)); + EXPECT_EQ(base::nullopt, driver_->Find(entries[22].guid)); + EXPECT_EQ(base::nullopt, driver_->Find(entries[23].guid)); + EXPECT_EQ(base::nullopt, driver_->Find(entries[24].guid)); +} + +TEST_F(DownloadServiceControllerImplTest, ExistingExternalDownload) { + Entry entry1 = test::BuildBasicEntry(Entry::State::ACTIVE); + Entry entry2 = test::BuildBasicEntry(Entry::State::ACTIVE); + Entry entry3 = test::BuildBasicEntry(Entry::State::ACTIVE); + entry3.scheduling_params.priority = SchedulingParams::Priority::UI; + + // Simulate an existing download the service knows about and one it does not. + DriverEntry dentry1 = + BuildDriverEntry(entry2, DriverEntry::State::IN_PROGRESS); + DriverEntry dentry2; + dentry2.guid = base::GenerateGUID(); + dentry2.state = DriverEntry::State::IN_PROGRESS; + + std::vector<Entry> entries = {entry1, entry2, entry3}; + std::vector<DriverEntry> dentries = {dentry1, dentry2}; + + EXPECT_CALL(*client_, OnServiceInitialized(false, _)).Times(1); + + // Set up the Controller. + device_status_listener_->SetDeviceStatus( + DeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED)); + + driver_->AddTestData(dentries); + InitializeController(); + store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>(entries)); + file_monitor_->TriggerInit(true); + driver_->MakeReady(); + task_runner_->RunUntilIdle(); + + EXPECT_EQ(Entry::State::ACTIVE, model_->Get(entry1.guid)->state); + EXPECT_EQ(Entry::State::ACTIVE, model_->Get(entry2.guid)->state); + EXPECT_EQ(Entry::State::ACTIVE, model_->Get(entry3.guid)->state); + + EXPECT_FALSE(driver_->Find(entry1.guid).has_value()); + + EXPECT_TRUE(driver_->Find(entry2.guid).has_value()); + EXPECT_TRUE(driver_->Find(entry2.guid).value().paused); + + EXPECT_TRUE(driver_->Find(entry3.guid).has_value()); + EXPECT_FALSE(driver_->Find(entry3.guid).value().paused); + + // Simulate a successful external download. + driver_->NotifyDownloadSucceeded(dentry2); + + EXPECT_TRUE(driver_->Find(entry1.guid).has_value()); + EXPECT_FALSE(driver_->Find(entry1.guid).value().paused); + EXPECT_FALSE(driver_->Find(entry2.guid).value().paused); + EXPECT_FALSE(driver_->Find(entry3.guid).value().paused); +} + +TEST_F(DownloadServiceControllerImplTest, NewExternalDownload) { + Entry entry1 = test::BuildBasicEntry(Entry::State::ACTIVE); + Entry entry2 = test::BuildBasicEntry(Entry::State::ACTIVE); + entry2.scheduling_params.priority = SchedulingParams::Priority::UI; + + DriverEntry dentry1 = + BuildDriverEntry(entry2, DriverEntry::State::IN_PROGRESS); + + std::vector<Entry> entries = {entry1, entry2}; + std::vector<DriverEntry> dentries = {dentry1}; + + EXPECT_CALL(*client_, OnServiceInitialized(false, _)).Times(1); + + // Set up the Controller. + device_status_listener_->SetDeviceStatus( + DeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED)); + + driver_->AddTestData(dentries); + InitializeController(); + store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>(entries)); + file_monitor_->TriggerInit(true); + driver_->MakeReady(); + task_runner_->RunUntilIdle(); + + EXPECT_EQ(Entry::State::ACTIVE, model_->Get(entry1.guid)->state); + EXPECT_EQ(Entry::State::ACTIVE, model_->Get(entry2.guid)->state); + + EXPECT_TRUE(driver_->Find(entry1.guid).has_value()); + EXPECT_FALSE(driver_->Find(entry1.guid).value().paused); + EXPECT_TRUE(driver_->Find(entry2.guid).has_value()); + EXPECT_FALSE(driver_->Find(entry2.guid).value().paused); + + DriverEntry dentry2; + dentry2.guid = base::GenerateGUID(); + dentry2.state = DriverEntry::State::IN_PROGRESS; + + // Simulate a newly created external download. + driver_->Start(RequestParams(), dentry2.guid, dentry2.current_file_path, + NO_TRAFFIC_ANNOTATION_YET); + + EXPECT_TRUE(driver_->Find(entry1.guid).value().paused); + EXPECT_FALSE(driver_->Find(entry2.guid).value().paused); + + // Simulate a paused external download. + dentry2.paused = true; + driver_->NotifyDownloadUpdate(dentry2); + + EXPECT_FALSE(driver_->Find(entry1.guid).value().paused); + EXPECT_FALSE(driver_->Find(entry2.guid).value().paused); + + // Simulate a resumed external download. + dentry2.paused = false; + driver_->NotifyDownloadUpdate(dentry2); + + EXPECT_TRUE(driver_->Find(entry1.guid).value().paused); + EXPECT_FALSE(driver_->Find(entry2.guid).value().paused); + + // Simulate a failed external download. + dentry2.state = DriverEntry::State::INTERRUPTED; + driver_->NotifyDownloadFailed(dentry2, FailureType::RECOVERABLE); + + EXPECT_FALSE(driver_->Find(entry1.guid).value().paused); + EXPECT_FALSE(driver_->Find(entry2.guid).value().paused); + + // Rebuild the download so we can simulate more. + dentry2.state = DriverEntry::State::IN_PROGRESS; + driver_->Start(RequestParams(), dentry2.guid, dentry2.current_file_path, + NO_TRAFFIC_ANNOTATION_YET); + + EXPECT_TRUE(driver_->Find(entry1.guid).value().paused); + EXPECT_FALSE(driver_->Find(entry2.guid).value().paused); + + // Simulate a successful external download. + dentry2.state = DriverEntry::State::COMPLETE; + driver_->NotifyDownloadSucceeded(dentry2); + + EXPECT_FALSE(driver_->Find(entry1.guid).value().paused); + EXPECT_FALSE(driver_->Find(entry2.guid).value().paused); +} + +TEST_F(DownloadServiceControllerImplTest, CancelTimeTest) { + Entry entry1 = test::BuildBasicEntry(); + entry1.state = Entry::State::ACTIVE; + entry1.create_time = base::Time::Now() - base::TimeDelta::FromSeconds(10); + entry1.scheduling_params.cancel_time = + base::Time::Now() - base::TimeDelta::FromSeconds(5); + + Entry entry2 = test::BuildBasicEntry(); + entry2.state = Entry::State::COMPLETE; + entry2.create_time = base::Time::Now() - base::TimeDelta::FromSeconds(10); + entry2.scheduling_params.cancel_time = + base::Time::Now() - base::TimeDelta::FromSeconds(2); + std::vector<Entry> entries = {entry1, entry2}; + + EXPECT_CALL(*client_, OnServiceInitialized(false, _)).Times(1); + + InitializeController(); + store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>(entries)); + file_monitor_->TriggerInit(true); + driver_->MakeReady(); + task_runner_->RunUntilIdle(); + + // At startup, timed out entries should be killed. + std::vector<Entry*> updated_entries = model_->PeekEntries(); + EXPECT_EQ(1u, updated_entries.size()); +} + +// Ensures no more downloads are activated if the number of downloads exceeds +// the max running download configuration. +TEST_F(DownloadServiceControllerImplTest, ThrottlingConfigMaxRunning) { + Entry entry1 = test::BuildBasicEntry(Entry::State::AVAILABLE); + Entry entry2 = test::BuildBasicEntry(Entry::State::ACTIVE); + std::vector<Entry> entries = {entry1, entry2}; + + EXPECT_CALL(*client_, OnServiceInitialized(false, _)).Times(1); + + // Setup the Configuration. + config_->max_concurrent_downloads = 1u; + config_->max_running_downloads = 1u; + + // Setup the controller. + InitializeController(); + store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>(entries)); + file_monitor_->TriggerInit(true); + store_->AutomaticallyTriggerAllFutureCallbacks(true); + + // Hit the max running configuration threshold, nothing should be called. + EXPECT_CALL(*scheduler_, Next(_, _)).Times(0); + EXPECT_CALL(*scheduler_, Reschedule(_)).Times(1); + driver_->MakeReady(); + task_runner_->RunUntilIdle(); + + EXPECT_EQ(Entry::State::AVAILABLE, model_->Get(entry1.guid)->state); +} + +// Ensures max concurrent download configuration considers both active and +// paused downloads. +TEST_F(DownloadServiceControllerImplTest, ThrottlingConfigMaxConcurrent) { + Entry entry1 = test::BuildBasicEntry(Entry::State::AVAILABLE); + Entry entry2 = test::BuildBasicEntry(Entry::State::AVAILABLE); + Entry entry3 = test::BuildBasicEntry(Entry::State::PAUSED); + std::vector<Entry> entries = {entry1, entry2, entry3}; + + EXPECT_CALL(*client_, OnServiceInitialized(false, _)).Times(1); + + // Setup the Configuration. + config_->max_concurrent_downloads = 2u; + config_->max_running_downloads = 1u; + + // Setup the controller. + InitializeController(); + store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>(entries)); + file_monitor_->TriggerInit(true); + store_->AutomaticallyTriggerAllFutureCallbacks(true); + + // Can have one more download due to max concurrent configuration. + testing::InSequence seq; + EXPECT_EQ(Entry::State::AVAILABLE, model_->Get(entry1.guid)->state); + EXPECT_CALL(*scheduler_, Next(_, _)) + .Times(1) + .WillOnce(Return(model_->Get(entry1.guid))) + .RetiresOnSaturation(); + // |scheduler_| will poll entry2 on next time, but it should not change the + // state of entry2 due to max running download configuration. + ON_CALL(*scheduler_, Next(_, _)) + .WillByDefault(Return(model_->Get(entry2.guid))); + + EXPECT_CALL(*scheduler_, Reschedule(_)).Times(1); + driver_->MakeReady(); + task_runner_->RunUntilIdle(); + + EXPECT_EQ(Entry::State::ACTIVE, model_->Get(entry1.guid)->state); + EXPECT_EQ(Entry::State::AVAILABLE, model_->Get(entry2.guid)->state); + EXPECT_EQ(Entry::State::PAUSED, model_->Get(entry3.guid)->state); +} + +} // namespace download diff --git a/chromium/components/download/internal/download_driver.h b/chromium/components/download/internal/download_driver.h index 88247d18d5d..40bac08f14d 100644 --- a/chromium/components/download/internal/download_driver.h +++ b/chromium/components/download/internal/download_driver.h @@ -5,10 +5,12 @@ #ifndef COMPONENTS_DOWNLOAD_INTERNAL_DOWNLOAD_DRIVER_H_ #define COMPONENTS_DOWNLOAD_INTERNAL_DOWNLOAD_DRIVER_H_ +#include <set> #include <string> #include "base/optional.h" #include "components/download/internal/driver_entry.h" +#include "net/traffic_annotation/network_traffic_annotation.h" namespace base { class FilePath; @@ -16,7 +18,16 @@ class FilePath; namespace download { -struct DownloadParams; +struct RequestParams; + +// Indicates the recovery type of a download. +enum class FailureType { + // Download failed due to an irrecoverable error. + NOT_RECOVERABLE = 0, + + // Download failed, but we might be able to recover if we try again. + RECOVERABLE = 1, +}; // The interface that includes all the operations to interact with low level // download library functionalities. @@ -31,35 +42,49 @@ class DownloadDriver { // when the low level download library is ready. virtual void OnDriverReady(bool success) = 0; + // Called asynchronously in response to a DownloadDriver::HardRecover call. + // If |success| is |false|, recovery of the DownloadDriver failed. + virtual void OnDriverHardRecoverComplete(bool success) = 0; + // Called when any download is created. virtual void OnDownloadCreated(const DriverEntry& download) = 0; // Called when any download is failed. |reason| is propagated from low level // download library. - virtual void OnDownloadFailed(const DriverEntry& download, int reason) = 0; + virtual void OnDownloadFailed(const DriverEntry& download, + FailureType failure_type) = 0; // Called when any download is successfully completed. - virtual void OnDownloadSucceeded(const DriverEntry& download, - const base::FilePath& path) = 0; + virtual void OnDownloadSucceeded(const DriverEntry& download) = 0; // Called when any download is updated. virtual void OnDownloadUpdated(const DriverEntry& download) = 0; }; + virtual ~DownloadDriver() = default; + // Initialize the driver to receive download updates. virtual void Initialize(Client* client) = 0; - // Returns if the driver is ready. Returns false when the driver is not - // initialized by the client, or low level download library has been shut - // down. + // Attempts to clean up and reset the DownloadDriver. It should remove all + // state relevant to the DownloadService. + virtual void HardRecover() = 0; + + // Returns if the driver is ready after the low level library has loaded all + // the data. Returns false when the driver is not initialized by the client, + // or low level download library has been shut down. virtual bool IsReady() const = 0; // Starts a new download. - virtual void Start(const DownloadParams& params) = 0; + virtual void Start( + const RequestParams& request_params, + const std::string& guid, + const base::FilePath& file_path, + const net::NetworkTrafficAnnotationTag& traffic_annotation) = 0; // Cancels an existing download, all data associated with this download should // be removed. - virtual void Cancel(const std::string& guid) = 0; + virtual void Remove(const std::string& guid) = 0; // Pauses the download. virtual void Pause(const std::string& guid) = 0; @@ -67,8 +92,12 @@ class DownloadDriver { // Resumes the download virtual void Resume(const std::string& guid) = 0; - // Find a download record from low level download library. + // Finds a download record from low level download library. virtual base::Optional<DriverEntry> Find(const std::string& guid) = 0; + + // Called to query the current set of active downloads. This doesn't + // necessarily mean downloads started by the service. + virtual std::set<std::string> GetActiveDownloads() = 0; }; } // namespace download diff --git a/chromium/components/download/internal/download_service_impl.cc b/chromium/components/download/internal/download_service_impl.cc index 44610a9a208..ceff12d729f 100644 --- a/chromium/components/download/internal/download_service_impl.cc +++ b/chromium/components/download/internal/download_service_impl.cc @@ -4,27 +4,158 @@ #include "components/download/internal/download_service_impl.h" +#include "base/bind.h" +#include "base/strings/string_util.h" +#include "components/download/internal/controller.h" +#include "components/download/internal/startup_status.h" +#include "components/download/internal/stats.h" + namespace download { -// static -DownloadService* DownloadService::Create( - const base::FilePath& storage_dir, - const scoped_refptr<base::SequencedTaskRunner>& background_task_runner) { - return new DownloadServiceImpl(Configuration::CreateFromFinch()); +DownloadServiceImpl::DownloadServiceImpl(std::unique_ptr<Configuration> config, + std::unique_ptr<Controller> controller) + : config_(std::move(config)), + controller_(std::move(controller)), + service_config_(config_.get()), + startup_completed_(false) { + controller_->Initialize(base::Bind( + &DownloadServiceImpl::OnControllerInitialized, base::Unretained(this))); } -DownloadServiceImpl::DownloadServiceImpl(std::unique_ptr<Configuration> config) - : config_(std::move(config)) {} - DownloadServiceImpl::~DownloadServiceImpl() = default; +const ServiceConfig& DownloadServiceImpl::GetConfig() { + return service_config_; +} + +void DownloadServiceImpl::OnStartScheduledTask( + DownloadTaskType task_type, + const TaskFinishedCallback& callback) { + if (startup_completed_) { + controller_->OnStartScheduledTask(task_type, callback); + return; + } + + pending_tasks_[task_type] = + base::Bind(&Controller::OnStartScheduledTask, + base::Unretained(controller_.get()), task_type, callback); +} + +bool DownloadServiceImpl::OnStopScheduledTask(DownloadTaskType task_type) { + if (startup_completed_) { + return controller_->OnStopScheduledTask(task_type); + } + + auto iter = pending_tasks_.find(task_type); + if (iter != pending_tasks_.end()) { + // We still need to run the callback in order to properly cleanup and notify + // the system by running the respective task finished callbacks. + iter->second.Run(); + pending_tasks_.erase(iter); + } + + return true; +} + +DownloadService::ServiceStatus DownloadServiceImpl::GetStatus() { + switch (controller_->GetState()) { + case Controller::State::CREATED: // Intentional fallthrough. + case Controller::State::INITIALIZING: // Intentional fallthrough. + case Controller::State::RECOVERING: + return DownloadService::ServiceStatus::STARTING_UP; + case Controller::State::READY: + return DownloadService::ServiceStatus::READY; + case Controller::State::UNAVAILABLE: // Intentional fallthrough. + default: + return DownloadService::ServiceStatus::UNAVAILABLE; + } +} + void DownloadServiceImpl::StartDownload(const DownloadParams& download_params) { + stats::LogServiceApiAction(download_params.client, + stats::ServiceApiAction::START_DOWNLOAD); + DCHECK_EQ(download_params.guid, base::ToUpperASCII(download_params.guid)); + stats::LogDownloadParams(download_params); + if (startup_completed_) { + controller_->StartDownload(download_params); + } else { + pending_actions_.push_back(base::Bind(&Controller::StartDownload, + base::Unretained(controller_.get()), + download_params)); + } } -void DownloadServiceImpl::PauseDownload(const std::string& guid) {} -void DownloadServiceImpl::ResumeDownload(const std::string& guid) {} -void DownloadServiceImpl::CancelDownload(const std::string& guid) {} + +void DownloadServiceImpl::PauseDownload(const std::string& guid) { + stats::LogServiceApiAction(controller_->GetOwnerOfDownload(guid), + stats::ServiceApiAction::PAUSE_DOWNLOAD); + DCHECK_EQ(guid, base::ToUpperASCII(guid)); + + if (startup_completed_) { + controller_->PauseDownload(guid); + } else { + pending_actions_.push_back(base::Bind( + &Controller::PauseDownload, base::Unretained(controller_.get()), guid)); + } +} + +void DownloadServiceImpl::ResumeDownload(const std::string& guid) { + stats::LogServiceApiAction(controller_->GetOwnerOfDownload(guid), + stats::ServiceApiAction::RESUME_DOWNLOAD); + DCHECK_EQ(guid, base::ToUpperASCII(guid)); + + if (startup_completed_) { + controller_->ResumeDownload(guid); + } else { + pending_actions_.push_back(base::Bind(&Controller::ResumeDownload, + base::Unretained(controller_.get()), + guid)); + } +} + +void DownloadServiceImpl::CancelDownload(const std::string& guid) { + stats::LogServiceApiAction(controller_->GetOwnerOfDownload(guid), + stats::ServiceApiAction::CANCEL_DOWNLOAD); + DCHECK_EQ(guid, base::ToUpperASCII(guid)); + + if (startup_completed_) { + controller_->CancelDownload(guid); + } else { + pending_actions_.push_back(base::Bind(&Controller::CancelDownload, + base::Unretained(controller_.get()), + guid)); + } +} + void DownloadServiceImpl::ChangeDownloadCriteria( const std::string& guid, - const SchedulingParams& params) {} + const SchedulingParams& params) { + stats::LogServiceApiAction(controller_->GetOwnerOfDownload(guid), + stats::ServiceApiAction::CHANGE_CRITERIA); + DCHECK_EQ(guid, base::ToUpperASCII(guid)); + + if (startup_completed_) { + controller_->ChangeDownloadCriteria(guid, params); + } else { + pending_actions_.push_back(base::Bind(&Controller::ChangeDownloadCriteria, + base::Unretained(controller_.get()), + guid, params)); + } +} + +void DownloadServiceImpl::OnControllerInitialized() { + while (!pending_actions_.empty()) { + auto callback = pending_actions_.front(); + callback.Run(); + pending_actions_.pop_front(); + } + + while (!pending_tasks_.empty()) { + auto iter = pending_tasks_.begin(); + iter->second.Run(); + pending_tasks_.erase(iter); + } + + startup_completed_ = true; +} } // namespace download diff --git a/chromium/components/download/internal/download_service_impl.h b/chromium/components/download/internal/download_service_impl.h index 319aea1976b..0333f9d0b1f 100644 --- a/chromium/components/download/internal/download_service_impl.h +++ b/chromium/components/download/internal/download_service_impl.h @@ -5,25 +5,35 @@ #ifndef COMPONENTS_DOWNLOAD_INTERNAL_DOWNLOAD_SERVICE_IMPL_H_ #define COMPONENTS_DOWNLOAD_INTERNAL_DOWNLOAD_SERVICE_IMPL_H_ +#include <deque> +#include <map> #include <memory> #include <string> #include "base/macros.h" #include "components/download/internal/config.h" +#include "components/download/internal/service_config_impl.h" #include "components/download/public/download_service.h" namespace download { +class Controller; struct DownloadParams; struct SchedulingParams; // The internal implementation of the DownloadService. class DownloadServiceImpl : public DownloadService { public: - DownloadServiceImpl(std::unique_ptr<Configuration> config); + DownloadServiceImpl(std::unique_ptr<Configuration> config, + std::unique_ptr<Controller> controller); ~DownloadServiceImpl() override; // DownloadService implementation. + const ServiceConfig& GetConfig() override; + void OnStartScheduledTask(DownloadTaskType task_type, + const TaskFinishedCallback& callback) override; + bool OnStopScheduledTask(DownloadTaskType task_type) override; + ServiceStatus GetStatus() override; void StartDownload(const DownloadParams& download_params) override; void PauseDownload(const std::string& guid) override; void ResumeDownload(const std::string& guid) override; @@ -32,8 +42,19 @@ class DownloadServiceImpl : public DownloadService { const SchedulingParams& params) override; private: + void OnControllerInitialized(); + + // config_ needs to be destructed after controller_ and service_config_ which + // hold onto references to it. std::unique_ptr<Configuration> config_; + std::unique_ptr<Controller> controller_; + ServiceConfigImpl service_config_; + + std::deque<base::Closure> pending_actions_; + std::map<DownloadTaskType, base::Closure> pending_tasks_; + bool startup_completed_; + DISALLOW_COPY_AND_ASSIGN(DownloadServiceImpl); }; diff --git a/chromium/components/download/internal/download_service_impl_unittest.cc b/chromium/components/download/internal/download_service_impl_unittest.cc new file mode 100644 index 00000000000..74090dfbca8 --- /dev/null +++ b/chromium/components/download/internal/download_service_impl_unittest.cc @@ -0,0 +1,145 @@ +// 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/download_service_impl.h" + +#include <memory> + +#include "base/macros.h" +#include "base/memory/ptr_util.h" +#include "base/strings/string_util.h" +#include "base/test/histogram_tester.h" +#include "base/test/test_simple_task_runner.h" +#include "base/threading/thread_task_runner_handle.h" +#include "components/download/internal/startup_status.h" +#include "components/download/internal/stats.h" +#include "components/download/internal/test/download_params_utils.h" +#include "components/download/internal/test/mock_controller.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using testing::_; +using testing::Return; + +namespace download { +namespace { + +class DownloadServiceImplTest : public testing::Test { + public: + DownloadServiceImplTest() + : controller_(nullptr), + task_runner_(new base::TestSimpleTaskRunner), + handle_(task_runner_) {} + ~DownloadServiceImplTest() override = default; + + void SetUp() override { + auto config = base::MakeUnique<Configuration>(); + auto controller = base::MakeUnique<test::MockController>(); + controller_ = controller.get(); + service_ = base::MakeUnique<DownloadServiceImpl>(std::move(config), + std::move(controller)); + } + + protected: + test::MockController* controller_; + std::unique_ptr<DownloadServiceImpl> service_; + scoped_refptr<base::TestSimpleTaskRunner> task_runner_; + base::ThreadTaskRunnerHandle handle_; + + DISALLOW_COPY_AND_ASSIGN(DownloadServiceImplTest); +}; + +} // namespace + +TEST_F(DownloadServiceImplTest, TestGetStatus) { + StartupStatus startup_status; + EXPECT_CALL(*controller_, GetState()) + .WillOnce(Return(Controller::State::INITIALIZING)) + .WillOnce(Return(Controller::State::READY)) + .WillOnce(Return(Controller::State::UNAVAILABLE)); + + EXPECT_EQ(DownloadService::ServiceStatus::STARTING_UP, service_->GetStatus()); + EXPECT_EQ(DownloadService::ServiceStatus::READY, service_->GetStatus()); + EXPECT_EQ(DownloadService::ServiceStatus::UNAVAILABLE, service_->GetStatus()); +} + +TEST_F(DownloadServiceImplTest, TestApiPassThrough) { + DownloadParams params = test::BuildBasicDownloadParams(); + // TODO(xingliu): Remove the limitation of upper case guid in + // |download_params|, see http://crbug.com/734818. + params.guid = base::ToUpperASCII(params.guid); + SchedulingParams scheduling_params; + scheduling_params.priority = SchedulingParams::Priority::UI; + + EXPECT_CALL(*controller_, GetOwnerOfDownload(_)) + .WillRepeatedly(Return(DownloadClient::TEST)); + + EXPECT_CALL(*controller_, StartDownload(_)).Times(0); + EXPECT_CALL(*controller_, PauseDownload(params.guid)).Times(0); + EXPECT_CALL(*controller_, ResumeDownload(params.guid)).Times(0); + EXPECT_CALL(*controller_, CancelDownload(params.guid)).Times(0); + EXPECT_CALL(*controller_, ChangeDownloadCriteria(params.guid, _)).Times(0); + + { + base::HistogramTester histogram_tester; + + service_->StartDownload(params); + + histogram_tester.ExpectBucketCount( + "Download.Service.Request.ClientAction", + static_cast<base::HistogramBase::Sample>( + stats::ServiceApiAction::START_DOWNLOAD), + 1); + histogram_tester.ExpectBucketCount( + "Download.Service.Request.ClientAction.__Test__", + static_cast<base::HistogramBase::Sample>( + stats::ServiceApiAction::START_DOWNLOAD), + 1); + histogram_tester.ExpectBucketCount( + "Download.Service.Request.BatteryRequirement", + static_cast<base::HistogramBase::Sample>( + params.scheduling_params.battery_requirements), + 1); + + histogram_tester.ExpectTotalCount("Download.Service.Request.ClientAction", + 1); + histogram_tester.ExpectTotalCount( + "Download.Service.Request.ClientAction.__Test__", 1); + } + service_->PauseDownload(params.guid); + service_->ResumeDownload(params.guid); + service_->CancelDownload(params.guid); + service_->ChangeDownloadCriteria(params.guid, scheduling_params); + task_runner_->RunUntilIdle(); + + testing::Sequence seq1; + EXPECT_CALL(*controller_, StartDownload(_)).Times(1).InSequence(seq1); + EXPECT_CALL(*controller_, PauseDownload(params.guid)) + .Times(1) + .InSequence(seq1); + EXPECT_CALL(*controller_, ResumeDownload(params.guid)) + .Times(1) + .InSequence(seq1); + EXPECT_CALL(*controller_, CancelDownload(params.guid)) + .Times(1) + .InSequence(seq1); + EXPECT_CALL(*controller_, ChangeDownloadCriteria(params.guid, _)) + .Times(1) + .InSequence(seq1); + + controller_->TriggerInitCompleted(); + task_runner_->RunUntilIdle(); + + EXPECT_CALL(*controller_, PauseDownload(params.guid)) + .Times(1) + .InSequence(seq1); + EXPECT_CALL(*controller_, ResumeDownload(params.guid)) + .Times(1) + .InSequence(seq1); + service_->PauseDownload(params.guid); + service_->ResumeDownload(params.guid); + task_runner_->RunUntilIdle(); +} + +} // namespace download diff --git a/chromium/components/download/internal/download_store.cc b/chromium/components/download/internal/download_store.cc new file mode 100644 index 00000000000..877547846df --- /dev/null +++ b/chromium/components/download/internal/download_store.cc @@ -0,0 +1,113 @@ +// 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/download_store.h" + +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "base/memory/ptr_util.h" +#include "components/download/internal/entry.h" +#include "components/download/internal/proto/entry.pb.h" +#include "components/download/internal/proto_conversions.h" +#include "components/leveldb_proto/proto_database_impl.h" + +namespace download { + +namespace { + +const char kDatabaseClientName[] = "DownloadService"; +using KeyVector = std::vector<std::string>; +using ProtoEntryVector = std::vector<protodb::Entry>; +using KeyProtoEntryVector = std::vector<std::pair<std::string, protodb::Entry>>; + +} // namespace + +DownloadStore::DownloadStore( + const base::FilePath& database_dir, + std::unique_ptr<leveldb_proto::ProtoDatabase<protodb::Entry>> db) + : db_(std::move(db)), + database_dir_(database_dir), + is_initialized_(false), + weak_factory_(this) {} + +DownloadStore::~DownloadStore() = default; + +bool DownloadStore::IsInitialized() { + return is_initialized_; +} + +void DownloadStore::Initialize(InitCallback callback) { + DCHECK(!IsInitialized()); + db_->InitWithOptions( + kDatabaseClientName, leveldb_proto::Options(database_dir_), + base::BindOnce(&DownloadStore::OnDatabaseInited, + weak_factory_.GetWeakPtr(), std::move(callback))); +} + +void DownloadStore::HardRecover(StoreCallback callback) { + is_initialized_ = false; + db_->Destroy(base::BindOnce(&DownloadStore::OnDatabaseDestroyed, + weak_factory_.GetWeakPtr(), std::move(callback))); +} + +void DownloadStore::OnDatabaseInited(InitCallback callback, bool success) { + if (!success) { + std::move(callback).Run(success, base::MakeUnique<std::vector<Entry>>()); + return; + } + + db_->LoadEntries(base::BindOnce(&DownloadStore::OnDatabaseLoaded, + weak_factory_.GetWeakPtr(), + std::move(callback))); +} + +void DownloadStore::OnDatabaseLoaded(InitCallback callback, + bool success, + std::unique_ptr<ProtoEntryVector> protos) { + if (!success) { + std::move(callback).Run(success, base::MakeUnique<std::vector<Entry>>()); + return; + } + + auto entries = ProtoConversions::EntryVectorFromProto(std::move(protos)); + is_initialized_ = true; + std::move(callback).Run(success, std::move(entries)); +} + +void DownloadStore::OnDatabaseDestroyed(StoreCallback callback, bool success) { + if (!success) { + std::move(callback).Run(success); + return; + } + + db_->InitWithOptions( + kDatabaseClientName, leveldb_proto::Options(database_dir_), + base::BindOnce(&DownloadStore::OnDatabaseInitedAfterDestroy, + weak_factory_.GetWeakPtr(), std::move(callback))); +} + +void DownloadStore::OnDatabaseInitedAfterDestroy(StoreCallback callback, + bool success) { + is_initialized_ = success; + std::move(callback).Run(success); +} + +void DownloadStore::Update(const Entry& entry, StoreCallback callback) { + DCHECK(IsInitialized()); + auto entries_to_save = base::MakeUnique<KeyProtoEntryVector>(); + protodb::Entry proto = ProtoConversions::EntryToProto(entry); + entries_to_save->emplace_back(entry.guid, std::move(proto)); + db_->UpdateEntries(std::move(entries_to_save), base::MakeUnique<KeyVector>(), + std::move(callback)); +} + +void DownloadStore::Remove(const std::string& guid, StoreCallback callback) { + DCHECK(IsInitialized()); + auto keys_to_remove = base::MakeUnique<KeyVector>(); + keys_to_remove->push_back(guid); + db_->UpdateEntries(base::MakeUnique<KeyProtoEntryVector>(), + std::move(keys_to_remove), std::move(callback)); +} + +} // namespace download diff --git a/chromium/components/download/internal/download_store.h b/chromium/components/download/internal/download_store.h new file mode 100644 index 00000000000..adb397b6b3c --- /dev/null +++ b/chromium/components/download/internal/download_store.h @@ -0,0 +1,60 @@ +// 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. + +#ifndef COMPONENTS_DOWNLOAD_INTERNAL_DOWNLOAD_STORE_H_ +#define COMPONENTS_DOWNLOAD_INTERNAL_DOWNLOAD_STORE_H_ + +#include <string> +#include <vector> + +#include "base/files/file_path.h" +#include "base/macros.h" +#include "base/memory/weak_ptr.h" +#include "components/download/internal/store.h" +#include "components/leveldb_proto/proto_database.h" + +namespace protodb { +class Entry; +} // namespace protodb + +namespace download { + +// DownloadStore provides a layer around a LevelDB proto database that persists +// the download request, scheduling params and metadata to the disk. The data is +// read during initialization and presented to the caller after converting to +// Entry entries. +class DownloadStore : public Store { + public: + DownloadStore( + const base::FilePath& database_dir, + std::unique_ptr<leveldb_proto::ProtoDatabase<protodb::Entry>> db); + ~DownloadStore() override; + + // Store implementation. + bool IsInitialized() override; + void Initialize(InitCallback callback) override; + void HardRecover(StoreCallback callback) override; + void Update(const Entry& entry, StoreCallback callback) override; + void Remove(const std::string& guid, StoreCallback callback) override; + + private: + void OnDatabaseInited(InitCallback callback, bool success); + void OnDatabaseLoaded(InitCallback callback, + bool success, + std::unique_ptr<std::vector<protodb::Entry>> protos); + void OnDatabaseDestroyed(StoreCallback callback, bool success); + void OnDatabaseInitedAfterDestroy(StoreCallback callback, bool success); + + std::unique_ptr<leveldb_proto::ProtoDatabase<protodb::Entry>> db_; + base::FilePath database_dir_; + bool is_initialized_; + + base::WeakPtrFactory<DownloadStore> weak_factory_; + + DISALLOW_COPY_AND_ASSIGN(DownloadStore); +}; + +} // namespace download + +#endif // COMPONENTS_DOWNLOAD_INTERNAL_DOWNLOAD_STORE_H_ diff --git a/chromium/components/download/internal/download_store_unittest.cc b/chromium/components/download/internal/download_store_unittest.cc new file mode 100644 index 00000000000..57b9997d554 --- /dev/null +++ b/chromium/components/download/internal/download_store_unittest.cc @@ -0,0 +1,324 @@ +// 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/download_store.h" + +#include <algorithm> + +#include "base/bind.h" +#include "base/callback.h" +#include "base/guid.h" +#include "base/memory/ptr_util.h" +#include "base/optional.h" +#include "components/download/internal/entry.h" +#include "components/download/internal/proto/entry.pb.h" +#include "components/download/internal/proto_conversions.h" +#include "components/download/internal/test/entry_utils.h" +#include "components/leveldb_proto/testing/fake_db.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using testing::_; + +namespace download { + +class DownloadStoreTest : public testing::Test { + public: + DownloadStoreTest() : db_(nullptr) {} + + ~DownloadStoreTest() override = default; + + void CreateDatabase() { + auto db = base::MakeUnique<leveldb_proto::test::FakeDB<protodb::Entry>>( + &db_entries_); + db_ = db.get(); + store_.reset(new DownloadStore( + base::FilePath(FILE_PATH_LITERAL("/test/db/fakepath")), std::move(db))); + } + + void InitCallback(std::vector<Entry>* loaded_entries, + bool success, + std::unique_ptr<std::vector<Entry>> entries) { + loaded_entries->swap(*entries); + } + + void LoadCallback(std::vector<protodb::Entry>* loaded_entries, + bool success, + std::unique_ptr<std::vector<protodb::Entry>> entries) { + loaded_entries->swap(*entries); + } + + void RecoverCallback(bool success) { hard_recover_result_ = success; } + + MOCK_METHOD1(StoreCallback, void(bool)); + + void PrepopulateSampleEntries() { + Entry item1 = test::BuildEntry(DownloadClient::TEST, base::GenerateGUID()); + Entry item2 = test::BuildEntry(DownloadClient::TEST, base::GenerateGUID()); + db_entries_.insert( + std::make_pair(item1.guid, ProtoConversions::EntryToProto(item1))); + db_entries_.insert( + std::make_pair(item2.guid, ProtoConversions::EntryToProto(item2))); + } + + protected: + std::map<std::string, protodb::Entry> db_entries_; + leveldb_proto::test::FakeDB<protodb::Entry>* db_; + std::unique_ptr<DownloadStore> store_; + base::Optional<bool> hard_recover_result_; + + DISALLOW_COPY_AND_ASSIGN(DownloadStoreTest); +}; + +TEST_F(DownloadStoreTest, Initialize) { + PrepopulateSampleEntries(); + CreateDatabase(); + ASSERT_FALSE(store_->IsInitialized()); + + std::vector<Entry> preloaded_entries; + store_->Initialize(base::Bind(&DownloadStoreTest::InitCallback, + base::Unretained(this), &preloaded_entries)); + db_->InitCallback(true); + db_->LoadCallback(true); + + ASSERT_TRUE(store_->IsInitialized()); + ASSERT_EQ(2u, preloaded_entries.size()); +} + +TEST_F(DownloadStoreTest, HardRecover) { + PrepopulateSampleEntries(); + CreateDatabase(); + ASSERT_FALSE(store_->IsInitialized()); + + std::vector<Entry> preloaded_entries; + store_->Initialize(base::Bind(&DownloadStoreTest::InitCallback, + base::Unretained(this), &preloaded_entries)); + db_->InitCallback(true); + db_->LoadCallback(true); + + ASSERT_TRUE(store_->IsInitialized()); + ASSERT_EQ(2u, preloaded_entries.size()); + + store_->HardRecover( + base::Bind(&DownloadStoreTest::RecoverCallback, base::Unretained(this))); + + ASSERT_FALSE(store_->IsInitialized()); + + db_->DestroyCallback(true); + db_->InitCallback(true); + + ASSERT_TRUE(store_->IsInitialized()); + ASSERT_TRUE(hard_recover_result_.has_value()); + ASSERT_TRUE(hard_recover_result_.value()); +} + +TEST_F(DownloadStoreTest, HardRecoverDestroyFails) { + PrepopulateSampleEntries(); + CreateDatabase(); + ASSERT_FALSE(store_->IsInitialized()); + + std::vector<Entry> preloaded_entries; + store_->Initialize(base::Bind(&DownloadStoreTest::InitCallback, + base::Unretained(this), &preloaded_entries)); + db_->InitCallback(true); + db_->LoadCallback(true); + + ASSERT_TRUE(store_->IsInitialized()); + ASSERT_EQ(2u, preloaded_entries.size()); + + store_->HardRecover( + base::Bind(&DownloadStoreTest::RecoverCallback, base::Unretained(this))); + + ASSERT_FALSE(store_->IsInitialized()); + + db_->DestroyCallback(false); + + ASSERT_FALSE(store_->IsInitialized()); + ASSERT_TRUE(hard_recover_result_.has_value()); + ASSERT_FALSE(hard_recover_result_.value()); +} + +TEST_F(DownloadStoreTest, HardRecoverInitFails) { + PrepopulateSampleEntries(); + CreateDatabase(); + ASSERT_FALSE(store_->IsInitialized()); + + std::vector<Entry> preloaded_entries; + store_->Initialize(base::Bind(&DownloadStoreTest::InitCallback, + base::Unretained(this), &preloaded_entries)); + db_->InitCallback(true); + db_->LoadCallback(true); + + ASSERT_TRUE(store_->IsInitialized()); + ASSERT_EQ(2u, preloaded_entries.size()); + + store_->HardRecover( + base::Bind(&DownloadStoreTest::RecoverCallback, base::Unretained(this))); + + ASSERT_FALSE(store_->IsInitialized()); + + db_->DestroyCallback(true); + db_->InitCallback(false); + + ASSERT_FALSE(store_->IsInitialized()); + ASSERT_TRUE(hard_recover_result_.has_value()); + ASSERT_FALSE(hard_recover_result_.value()); +} + +TEST_F(DownloadStoreTest, Update) { + PrepopulateSampleEntries(); + CreateDatabase(); + + std::vector<Entry> preloaded_entries; + store_->Initialize(base::Bind(&DownloadStoreTest::InitCallback, + base::Unretained(this), &preloaded_entries)); + db_->InitCallback(true); + db_->LoadCallback(true); + ASSERT_TRUE(store_->IsInitialized()); + ASSERT_EQ(2u, preloaded_entries.size()); + + Entry item1 = test::BuildEntry(DownloadClient::TEST, base::GenerateGUID()); + Entry item2 = test::BuildEntry(DownloadClient::TEST, base::GenerateGUID()); + EXPECT_CALL(*this, StoreCallback(true)).Times(2); + store_->Update(item1, base::Bind(&DownloadStoreTest::StoreCallback, + base::Unretained(this))); + db_->UpdateCallback(true); + store_->Update(item2, base::Bind(&DownloadStoreTest::StoreCallback, + base::Unretained(this))); + db_->UpdateCallback(true); + + // Query the database directly and check for the entry. + auto protos = base::MakeUnique<std::vector<protodb::Entry>>(); + db_->LoadEntries(base::Bind(&DownloadStoreTest::LoadCallback, + base::Unretained(this), protos.get())); + db_->LoadCallback(true); + ASSERT_EQ(4u, protos->size()); + ASSERT_TRUE(test::CompareEntryList( + {preloaded_entries[0], preloaded_entries[1], item1, item2}, + *ProtoConversions::EntryVectorFromProto(std::move(protos)))); +} + +TEST_F(DownloadStoreTest, Remove) { + PrepopulateSampleEntries(); + CreateDatabase(); + + std::vector<Entry> preloaded_entries; + store_->Initialize(base::Bind(&DownloadStoreTest::InitCallback, + base::Unretained(this), &preloaded_entries)); + db_->InitCallback(true); + db_->LoadCallback(true); + ASSERT_EQ(2u, preloaded_entries.size()); + + // Remove the entry. + EXPECT_CALL(*this, StoreCallback(true)).Times(1); + store_->Remove( + preloaded_entries[0].guid, + base::Bind(&DownloadStoreTest::StoreCallback, base::Unretained(this))); + db_->UpdateCallback(true); + + // Query the database directly and check for the entry removed. + auto protos = base::MakeUnique<std::vector<protodb::Entry>>(); + db_->LoadEntries(base::Bind(&DownloadStoreTest::LoadCallback, + base::Unretained(this), protos.get())); + db_->LoadCallback(true); + ASSERT_EQ(1u, protos->size()); + ASSERT_TRUE(test::CompareEntryList( + {preloaded_entries[1]}, + *ProtoConversions::EntryVectorFromProto(std::move(protos)))); +} + +TEST_F(DownloadStoreTest, InitializeFailed) { + PrepopulateSampleEntries(); + CreateDatabase(); + + std::vector<Entry> preloaded_entries; + store_->Initialize(base::Bind(&DownloadStoreTest::InitCallback, + base::Unretained(this), &preloaded_entries)); + db_->InitCallback(false); + ASSERT_FALSE(store_->IsInitialized()); + ASSERT_TRUE(preloaded_entries.empty()); +} + +TEST_F(DownloadStoreTest, InitialLoadFailed) { + PrepopulateSampleEntries(); + CreateDatabase(); + + std::vector<Entry> preloaded_entries; + store_->Initialize(base::Bind(&DownloadStoreTest::InitCallback, + base::Unretained(this), &preloaded_entries)); + db_->InitCallback(true); + db_->LoadCallback(false); + ASSERT_FALSE(store_->IsInitialized()); + ASSERT_TRUE(preloaded_entries.empty()); +} + +TEST_F(DownloadStoreTest, UnsuccessfulUpdateOrRemove) { + Entry item1 = test::BuildEntry(DownloadClient::TEST, base::GenerateGUID()); + CreateDatabase(); + + std::vector<Entry> entries; + store_->Initialize(base::Bind(&DownloadStoreTest::InitCallback, + base::Unretained(this), &entries)); + db_->InitCallback(true); + db_->LoadCallback(true); + ASSERT_TRUE(store_->IsInitialized()); + ASSERT_TRUE(entries.empty()); + + // Update failed. + EXPECT_CALL(*this, StoreCallback(false)).Times(1); + store_->Update(item1, base::Bind(&DownloadStoreTest::StoreCallback, + base::Unretained(this))); + db_->UpdateCallback(false); + + // Remove failed. + EXPECT_CALL(*this, StoreCallback(false)).Times(1); + store_->Remove(item1.guid, base::Bind(&DownloadStoreTest::StoreCallback, + base::Unretained(this))); + db_->UpdateCallback(false); +} + +TEST_F(DownloadStoreTest, AddThenRemove) { + CreateDatabase(); + + std::vector<Entry> entries; + store_->Initialize(base::Bind(&DownloadStoreTest::InitCallback, + base::Unretained(this), &entries)); + db_->InitCallback(true); + db_->LoadCallback(true); + ASSERT_TRUE(entries.empty()); + + Entry item1 = test::BuildEntry(DownloadClient::TEST, base::GenerateGUID()); + Entry item2 = test::BuildEntry(DownloadClient::TEST, base::GenerateGUID()); + EXPECT_CALL(*this, StoreCallback(true)).Times(2); + store_->Update(item1, base::Bind(&DownloadStoreTest::StoreCallback, + base::Unretained(this))); + db_->UpdateCallback(true); + store_->Update(item2, base::Bind(&DownloadStoreTest::StoreCallback, + base::Unretained(this))); + db_->UpdateCallback(true); + + // Query the database directly and check for the entry. + auto protos = base::MakeUnique<std::vector<protodb::Entry>>(); + db_->LoadEntries(base::Bind(&DownloadStoreTest::LoadCallback, + base::Unretained(this), protos.get())); + db_->LoadCallback(true); + ASSERT_EQ(2u, protos->size()); + + // Remove the entry. + EXPECT_CALL(*this, StoreCallback(true)).Times(1); + store_->Remove(item1.guid, base::Bind(&DownloadStoreTest::StoreCallback, + base::Unretained(this))); + db_->UpdateCallback(true); + + // Query the database directly and check for the entry removed. + protos->clear(); + db_->LoadEntries(base::Bind(&DownloadStoreTest::LoadCallback, + base::Unretained(this), protos.get())); + db_->LoadCallback(true); + ASSERT_EQ(1u, protos->size()); + ASSERT_TRUE(test::CompareEntryList( + {item2}, *ProtoConversions::EntryVectorFromProto(std::move(protos)))); +} + +} // namespace download diff --git a/chromium/components/download/internal/driver_entry.h b/chromium/components/download/internal/driver_entry.h index a21a4312c9e..c7b41632c8d 100644 --- a/chromium/components/download/internal/driver_entry.h +++ b/chromium/components/download/internal/driver_entry.h @@ -6,8 +6,11 @@ #define COMPONENTS_DOWNLOAD_INTERNAL_DRIVER_ENTRY_H_ #include <string> +#include <vector> +#include "base/files/file_path.h" #include "base/memory/ref_counted.h" +#include "url/gurl.h" namespace net { class HttpResponseHeaders; @@ -52,8 +55,22 @@ struct DriverEntry { // http header is not presented. uint64_t expected_total_size; + // The physical file path for the download. It can be different from the + // target file path requested while the file is downloading, as it may + // download to a temporary path. After completion, this would be set to the + // target file path. + base::FilePath current_file_path; + + // Time the download was marked as complete, base::Time() if the download is + // not yet complete. + base::Time completion_time; + // The response headers for the most recent download request. scoped_refptr<const net::HttpResponseHeaders> response_headers; + + // The url chain of the download. Download may encounter redirects, and + // fetches the content from the last url in the chain. + std::vector<GURL> url_chain; }; } // namespace download diff --git a/chromium/components/download/internal/entry.cc b/chromium/components/download/internal/entry.cc index 56d819c39d8..4df4757a0b0 100644 --- a/chromium/components/download/internal/entry.cc +++ b/chromium/components/download/internal/entry.cc @@ -6,8 +6,35 @@ namespace download { -Entry::Entry() = default; +Entry::Entry() : attempt_count(0) {} Entry::Entry(const Entry& other) = default; + +Entry::Entry(const DownloadParams& params) + : client(params.client), + guid(params.guid), + create_time(base::Time::Now()), + scheduling_params(params.scheduling_params), + request_params(params.request_params), + attempt_count(0) {} + Entry::~Entry() = default; +bool Entry::operator==(const Entry& other) const { + return client == other.client && guid == other.guid && + scheduling_params.cancel_time == other.scheduling_params.cancel_time && + scheduling_params.network_requirements == + other.scheduling_params.network_requirements && + scheduling_params.battery_requirements == + other.scheduling_params.battery_requirements && + scheduling_params.priority == other.scheduling_params.priority && + request_params.url == other.request_params.url && + request_params.method == other.request_params.method && + request_params.request_headers.ToString() == + other.request_params.request_headers.ToString() && + state == other.state && target_file_path == other.target_file_path && + create_time == other.create_time && + completion_time == other.completion_time && + attempt_count == other.attempt_count; +} + } // namespace download diff --git a/chromium/components/download/internal/entry.h b/chromium/components/download/internal/entry.h index 9ba0c427527..20766317889 100644 --- a/chromium/components/download/internal/entry.h +++ b/chromium/components/download/internal/entry.h @@ -5,6 +5,7 @@ #ifndef COMPONENTS_DOWNLOAD_INTERNAL_ENTRY_H_ #define COMPONENTS_DOWNLOAD_INTERNAL_ENTRY_H_ +#include "base/time/time.h" #include "components/download/public/client.h" #include "components/download/public/clients.h" #include "components/download/public/download_params.h" @@ -32,25 +33,30 @@ struct Entry { // be run until it is resumed by the Client. PAUSED = 3, - // The download is 'complete' by some definition of that term (could have - // failed, could have succeeded, etc.). It is ready to have UMA logs saved. + // The download is 'complete' and successful. At this point we are leaving + // this entry around to make sure the files on disk are cleaned up. COMPLETE = 4, - // The download is finished. We are leaving this entry around to make sure - // the files on disk are cleaned up. - WATCHDOG = 5, + // The count of entries for the enum. + COUNT = 5, }; Entry(); Entry(const Entry& other); + explicit Entry(const DownloadParams& params); ~Entry(); + bool operator==(const Entry& other) const; + // The feature that is requesting this download. DownloadClient client = DownloadClient::INVALID; // A unique GUID that represents this download. See | base::GenerateGUID()|. std::string guid; + // The time when the entry is created. + base::Time create_time; + // The parameters that determine under what device conditions this download // will occur. SchedulingParams scheduling_params; @@ -61,6 +67,16 @@ struct Entry { // The state of the download to help the scheduler and loggers make the right // decisions about the download object. State state = State::NEW; + + // Target file path for this download. + base::FilePath target_file_path; + + // Time the download was marked as complete, base::Time() if the download is + // not yet complete. + base::Time completion_time; + + // Stores the number of retries for this download. + uint32_t attempt_count; }; } // namespace download diff --git a/chromium/components/download/internal/entry_utils.cc b/chromium/components/download/internal/entry_utils.cc new file mode 100644 index 00000000000..35aa3ede353 --- /dev/null +++ b/chromium/components/download/internal/entry_utils.cc @@ -0,0 +1,67 @@ +// 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/entry_utils.h" + +#include "components/download/internal/entry.h" + +namespace download { +namespace util { + +uint32_t GetNumberOfEntriesForClient(DownloadClient client, + const std::vector<Entry*>& entries) { + uint32_t count = 0; + for (auto* entry : entries) + if (entry->client == client) + count++; + + return count; +} + +std::map<DownloadClient, std::vector<std::string>> MapEntriesToClients( + const std::set<DownloadClient>& clients, + const std::vector<Entry*>& entries, + const std::set<Entry::State>& ignored_states) { + std::map<DownloadClient, std::vector<std::string>> categorized; + + for (auto* entry : entries) { + if (ignored_states.find(entry->state) != ignored_states.end()) + continue; + + DownloadClient client = entry->client; + if (clients.find(client) == clients.end()) + client = DownloadClient::INVALID; + + categorized[client].push_back(entry->guid); + } + + return categorized; +} + +Criteria GetSchedulingCriteria(const Model::EntryList& entries) { + Criteria criteria; + for (auto* const entry : entries) { + DCHECK(entry); + const SchedulingParams& scheduling_params = entry->scheduling_params; + if (scheduling_params.battery_requirements == + SchedulingParams::BatteryRequirements::BATTERY_INSENSITIVE) { + criteria.requires_battery_charging = false; + } + if (scheduling_params.network_requirements == + SchedulingParams::NetworkRequirements::NONE) { + criteria.requires_unmetered_network = false; + } + } + return criteria; +} + +bool EntryBetterThan(const Entry& lhs, const Entry& rhs) { + return lhs.scheduling_params.priority > rhs.scheduling_params.priority || + (lhs.scheduling_params.priority == rhs.scheduling_params.priority && + lhs.scheduling_params.cancel_time < + rhs.scheduling_params.cancel_time); +} + +} // namespace util +} // namespace download diff --git a/chromium/components/download/internal/entry_utils.h b/chromium/components/download/internal/entry_utils.h new file mode 100644 index 00000000000..780b49467fb --- /dev/null +++ b/chromium/components/download/internal/entry_utils.h @@ -0,0 +1,49 @@ +// 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. + +#ifndef COMPONENTS_DOWNLOAD_INTERNAL_ENTRY_UTILS_H_ +#define COMPONENTS_DOWNLOAD_INTERNAL_ENTRY_UTILS_H_ + +#include <map> +#include <set> +#include <string> +#include <vector> + +#include "components/download/internal/model.h" +#include "components/download/internal/scheduler/device_status.h" +#include "components/download/public/clients.h" + +namespace download { + +struct Entry; + +namespace util { + +// Helper method to return the number of Entry objects in |entries| are +// associated with |client|. +uint32_t GetNumberOfEntriesForClient(DownloadClient client, + const std::vector<Entry*>& entries); + +// Effectively runs a map reduce to turn a list of Entry objects into a map of +// [Client Id] -> List of entries. Any Entry in |entries| that does not have a +// matching DownloadClient in |clients| will be put in the +// DownloadClient::INVALID bucket. Any Entry in |entries| with an Entry::State +// that is in |ignored_states| will not be included. +std::map<DownloadClient, std::vector<std::string>> MapEntriesToClients( + const std::set<DownloadClient>& clients, + const std::vector<Entry*>& entries, + const std::set<Entry::State>& ignored_states); + +// Gets the least strict scheduling criteria from |entries|, the criteria is +// used to schedule platform background tasks. +Criteria GetSchedulingCriteria(const Model::EntryList& entries); + +// Returns if |lhs| entry is a better candidate to be the next download than +// |rhs| based on their priority and cancel time. +bool EntryBetterThan(const Entry& lhs, const Entry& rhs); + +} // namespace util +} // namespace download + +#endif // COMPONENTS_DOWNLOAD_INTERNAL_ENTRY_UTILS_H_ diff --git a/chromium/components/download/internal/entry_utils_unittest.cc b/chromium/components/download/internal/entry_utils_unittest.cc new file mode 100644 index 00000000000..472151dc50c --- /dev/null +++ b/chromium/components/download/internal/entry_utils_unittest.cc @@ -0,0 +1,119 @@ +// 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/entry_utils.h" + +#include <algorithm> + +#include "base/memory/ptr_util.h" +#include "components/download/internal/test/entry_utils.h" +#include "components/download/public/clients.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace download { + +TEST(DownloadServiceEntryUtilsTest, TestGetNumberOfEntriesForClient_NoEntries) { + Entry entry1 = test::BuildBasicEntry(); + Entry entry2 = test::BuildBasicEntry(); + Entry entry3 = test::BuildBasicEntry(); + + std::vector<Entry*> entries = {&entry1, &entry2, &entry3}; + + EXPECT_EQ( + 0U, util::GetNumberOfEntriesForClient(DownloadClient::INVALID, entries)); + EXPECT_EQ(3U, + util::GetNumberOfEntriesForClient(DownloadClient::TEST, entries)); +} + +TEST(DownloadServiceEntryUtilsTest, MapEntriesToClients) { + Entry entry1 = test::BuildBasicEntry(); + Entry entry2 = test::BuildBasicEntry(); + Entry entry3 = test::BuildBasicEntry(); + Entry entry4 = test::BuildBasicEntry(Entry::State::COMPLETE); + Entry entry5 = test::BuildBasicEntry(Entry::State::AVAILABLE); + + std::vector<Entry*> entries = {&entry1, &entry2, &entry3, &entry4, &entry5}; + std::vector<std::string> expected_list = { + entry1.guid, entry2.guid, entry3.guid, entry4.guid, entry5.guid}; + std::vector<std::string> expected_pruned_list = {entry1.guid, entry2.guid, + entry3.guid, entry5.guid}; + // If DownloadClient::TEST isn't a valid Client, all of the associated entries + // should move to the DownloadClient::INVALID bucket. + auto mapped1 = util::MapEntriesToClients(std::set<DownloadClient>(), entries, + std::set<Entry::State>()); + EXPECT_EQ(1U, mapped1.size()); + EXPECT_NE(mapped1.end(), mapped1.find(DownloadClient::INVALID)); + EXPECT_EQ(mapped1.end(), mapped1.find(DownloadClient::TEST)); + + auto list1 = mapped1.find(DownloadClient::INVALID)->second; + EXPECT_EQ(5U, list1.size()); + EXPECT_TRUE( + std::equal(expected_list.begin(), expected_list.end(), list1.begin())); + + // If DownloadClient::TEST is a valid Client, it should have the associated + // entries. + std::set<DownloadClient> clients = {DownloadClient::TEST}; + auto mapped2 = + util::MapEntriesToClients(clients, entries, std::set<Entry::State>()); + EXPECT_EQ(1U, mapped2.size()); + EXPECT_NE(mapped2.end(), mapped2.find(DownloadClient::TEST)); + EXPECT_EQ(mapped2.end(), mapped2.find(DownloadClient::INVALID)); + + auto list2 = mapped2.find(DownloadClient::TEST)->second; + EXPECT_EQ(5U, list2.size()); + EXPECT_TRUE( + std::equal(expected_list.begin(), expected_list.end(), list2.begin())); + + // If we are pruning entries with certain states, make sure those entries + // don't show up in the results. + std::set<Entry::State> ignored_states = {Entry::State::ACTIVE, + Entry::State::COMPLETE}; + auto mapped3 = util::MapEntriesToClients(clients, entries, ignored_states); + EXPECT_EQ(1U, mapped3.size()); + auto list3 = mapped3.find(DownloadClient::TEST)->second; + EXPECT_EQ(4U, list3.size()); + EXPECT_TRUE(std::equal(expected_pruned_list.begin(), + expected_pruned_list.end(), list3.begin())); +} + +TEST(DownloadServiceEntryUtilsTest, GetSchedulingCriteria) { + Entry entry1 = test::BuildBasicEntry(); + Entry entry2 = test::BuildBasicEntry(); + Entry entry3 = test::BuildBasicEntry(); + Entry entry4 = test::BuildBasicEntry(); + + entry1.scheduling_params.network_requirements = + SchedulingParams::NetworkRequirements::UNMETERED; + entry1.scheduling_params.battery_requirements = + SchedulingParams::BatteryRequirements::BATTERY_SENSITIVE; + + entry2.scheduling_params.network_requirements = + SchedulingParams::NetworkRequirements::NONE; + entry2.scheduling_params.battery_requirements = + SchedulingParams::BatteryRequirements::BATTERY_SENSITIVE; + + entry3.scheduling_params.network_requirements = + SchedulingParams::NetworkRequirements::UNMETERED; + entry3.scheduling_params.battery_requirements = + SchedulingParams::BatteryRequirements::BATTERY_INSENSITIVE; + + entry4.scheduling_params.network_requirements = + SchedulingParams::NetworkRequirements::NONE; + entry4.scheduling_params.battery_requirements = + SchedulingParams::BatteryRequirements::BATTERY_INSENSITIVE; + + Model::EntryList list1 = {&entry1}; + Model::EntryList list2 = {&entry1, &entry2}; + Model::EntryList list3 = {&entry1, &entry3}; + Model::EntryList list4 = {&entry1, &entry2, &entry3}; + Model::EntryList list5 = {&entry1, &entry4}; + + EXPECT_EQ(Criteria(true, true), util::GetSchedulingCriteria(list1)); + EXPECT_EQ(Criteria(true, false), util::GetSchedulingCriteria(list2)); + EXPECT_EQ(Criteria(false, true), util::GetSchedulingCriteria(list3)); + EXPECT_EQ(Criteria(false, false), util::GetSchedulingCriteria(list4)); + EXPECT_EQ(Criteria(false, false), util::GetSchedulingCriteria(list5)); +} + +} // namespace download diff --git a/chromium/components/download/internal/file_monitor.h b/chromium/components/download/internal/file_monitor.h new file mode 100644 index 00000000000..9df409c1e70 --- /dev/null +++ b/chromium/components/download/internal/file_monitor.h @@ -0,0 +1,58 @@ +// 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. + +#ifndef COMPONENTS_DOWNLOAD_INTERNAL_FILE_MONITOR_H_ +#define COMPONENTS_DOWNLOAD_INTERNAL_FILE_MONITOR_H_ + +#include <memory> +#include <set> +#include <string> +#include <vector> + +#include "components/download/internal/model.h" +#include "components/download/internal/stats.h" + +namespace base { +class FilePath; +} // namespace base + +namespace download { + +struct DriverEntry; +struct Entry; + +// An utility class containing various file cleanup methods. +class FileMonitor { + public: + using InitCallback = base::Callback<void(bool)>; + + // Creates the file directory for the downloads if it doesn't exist. + virtual void Initialize(const InitCallback& callback) = 0; + + // Deletes the files in storage directory that are not related to any entries + // in either database. + virtual void DeleteUnknownFiles( + const Model::EntryList& known_entries, + const std::vector<DriverEntry>& known_driver_entries) = 0; + + // Deletes the files for the database entries which have been completed and + // ready for cleanup. Returns the entries eligible for clean up. + virtual std::vector<Entry*> CleanupFilesForCompletedEntries( + const Model::EntryList& entries, + const base::Closure& completion_callback) = 0; + + // Deletes a list of files and logs UMA. + virtual void DeleteFiles(const std::set<base::FilePath>& files_to_remove, + stats::FileCleanupReason reason) = 0; + + // Deletes all files in the download service directory. This is a hard reset + // on this directory. + virtual void HardRecover(const InitCallback& callback) = 0; + + virtual ~FileMonitor() = default; +}; + +} // namespace download + +#endif // COMPONENTS_DOWNLOAD_INTERNAL_FILE_MONITOR_H_ diff --git a/chromium/components/download/internal/file_monitor_impl.cc b/chromium/components/download/internal/file_monitor_impl.cc new file mode 100644 index 00000000000..639db4b9f22 --- /dev/null +++ b/chromium/components/download/internal/file_monitor_impl.cc @@ -0,0 +1,219 @@ +// 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/file_monitor_impl.h" + +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "base/files/file_enumerator.h" +#include "base/files/file_util.h" +#include "base/memory/ptr_util.h" +#include "base/stl_util.h" +#include "base/sys_info.h" +#include "base/task_runner_util.h" +#include "base/threading/thread_restrictions.h" + +namespace download { + +namespace { + +// Helper function to calculate total file size in a directory, the total +// disk space and free disk space of the volume that contains that directory. +// Returns false if failed to query disk space or total disk space is empty. +bool CalculateDiskUtilization(const base::FilePath& file_dir, + int64_t& total_disk_space, + int64_t& free_disk_space, + int64_t& files_size) { + base::ThreadRestrictions::AssertIOAllowed(); + base::FileEnumerator file_enumerator(file_dir, false /* recursive */, + base::FileEnumerator::FILES); + + int64_t size = 0; + // Compute the total size of all files in |file_dir|. + for (base::FilePath path = file_enumerator.Next(); !path.value().empty(); + path = file_enumerator.Next()) { + if (!base::GetFileSize(path, &size)) { + DVLOG(1) << "File size query failed."; + return false; + } + files_size += size; + } + + // Disk space of the volume that |file_dir| belongs to. + total_disk_space = base::SysInfo::AmountOfTotalDiskSpace(file_dir); + free_disk_space = base::SysInfo::AmountOfFreeDiskSpace(file_dir); + if (total_disk_space == -1 || free_disk_space == -1) { + DVLOG(1) << "System disk space query failed."; + return false; + } + + if (total_disk_space == 0) { + DVLOG(1) << "Empty total system disk space."; + return false; + } + return true; +} + +// Creates the download directory if it doesn't exist. +bool InitializeAndCreateDownloadDirectory(const base::FilePath& dir_path) { + // Create the download directory. + bool success = base::PathExists(dir_path); + if (!success) { + base::File::Error error = base::File::Error::FILE_OK; + success = base::CreateDirectoryAndGetError(dir_path, &error); + if (!success) + stats::LogsFileDirectoryCreationError(error); + } + // Records disk utilization histograms. + if (success) { + int64_t files_size = 0, total_disk_space = 0, free_disk_space = 0; + if (CalculateDiskUtilization(dir_path, total_disk_space, free_disk_space, + files_size)) { + stats::LogFileDirDiskUtilization(total_disk_space, free_disk_space, + files_size); + } + } + + return success; +} + +void GetFilesInDirectory(const base::FilePath& directory, + std::set<base::FilePath>& paths_out) { + base::ThreadRestrictions::AssertIOAllowed(); + base::FileEnumerator file_enumerator(directory, false /* recursive */, + base::FileEnumerator::FILES); + + for (base::FilePath path = file_enumerator.Next(); !path.value().empty(); + path = file_enumerator.Next()) { + paths_out.insert(path); + } +} + +void DeleteFilesOnFileThread(const std::set<base::FilePath>& paths, + stats::FileCleanupReason reason) { + base::ThreadRestrictions::AssertIOAllowed(); + int num_delete_attempted = 0; + int num_delete_failed = 0; + int num_delete_by_external = 0; + for (const base::FilePath& path : paths) { + if (!base::PathExists(path)) { + num_delete_by_external++; + continue; + } + + num_delete_attempted++; + DCHECK(!base::DirectoryExists(path)); + + if (!base::DeleteFile(path, false /* recursive */)) { + num_delete_failed++; + } + } + + stats::LogFileCleanupStatus(reason, num_delete_attempted, num_delete_failed, + num_delete_by_external); +} + +void DeleteUnknownFilesOnFileThread( + const base::FilePath& directory, + const std::set<base::FilePath>& download_file_paths) { + base::ThreadRestrictions::AssertIOAllowed(); + std::set<base::FilePath> files_in_dir; + GetFilesInDirectory(directory, files_in_dir); + + std::set<base::FilePath> files_to_remove = + base::STLSetDifference<std::set<base::FilePath>>(files_in_dir, + download_file_paths); + DeleteFilesOnFileThread(files_to_remove, stats::FileCleanupReason::UNKNOWN); +} + +bool HardRecoverOnFileThread(const base::FilePath& directory) { + base::ThreadRestrictions::AssertIOAllowed(); + std::set<base::FilePath> files_in_dir; + GetFilesInDirectory(directory, files_in_dir); + DeleteFilesOnFileThread(files_in_dir, + stats::FileCleanupReason::HARD_RECOVERY); + return InitializeAndCreateDownloadDirectory(directory); +} + +} // namespace + +FileMonitorImpl::FileMonitorImpl( + const base::FilePath& download_file_dir, + const scoped_refptr<base::SequencedTaskRunner>& file_thread_task_runner, + base::TimeDelta file_keep_alive_time) + : download_file_dir_(download_file_dir), + file_keep_alive_time_(file_keep_alive_time), + file_thread_task_runner_(file_thread_task_runner), + weak_factory_(this) {} + +FileMonitorImpl::~FileMonitorImpl() = default; + +void FileMonitorImpl::Initialize(const InitCallback& callback) { + base::PostTaskAndReplyWithResult( + file_thread_task_runner_.get(), FROM_HERE, + base::Bind(&InitializeAndCreateDownloadDirectory, download_file_dir_), + callback); +} + +void FileMonitorImpl::DeleteUnknownFiles( + const Model::EntryList& known_entries, + const std::vector<DriverEntry>& known_driver_entries) { + std::set<base::FilePath> download_file_paths; + for (Entry* entry : known_entries) { + download_file_paths.insert(entry->target_file_path); + } + + for (const DriverEntry& driver_entry : known_driver_entries) { + download_file_paths.insert(driver_entry.current_file_path); + } + + file_thread_task_runner_->PostTask( + FROM_HERE, base::Bind(&DeleteUnknownFilesOnFileThread, download_file_dir_, + download_file_paths)); +} + +std::vector<Entry*> FileMonitorImpl::CleanupFilesForCompletedEntries( + const Model::EntryList& entries, + const base::Closure& completion_callback) { + std::vector<Entry*> entries_to_remove; + std::set<base::FilePath> files_to_remove; + for (auto* entry : entries) { + if (!ReadyForCleanup(entry)) + continue; + + entries_to_remove.push_back(entry); + files_to_remove.insert(entry->target_file_path); + + // TODO(xingliu): Consider logs life time after the file being deleted on + // the file thread. + stats::LogFileLifeTime(base::Time::Now() - entry->completion_time); + } + + file_thread_task_runner_->PostTaskAndReply( + FROM_HERE, + base::Bind(&DeleteFilesOnFileThread, files_to_remove, + stats::FileCleanupReason::TIMEOUT), + completion_callback); + return entries_to_remove; +} + +void FileMonitorImpl::DeleteFiles( + const std::set<base::FilePath>& files_to_remove, + stats::FileCleanupReason reason) { + file_thread_task_runner_->PostTask( + FROM_HERE, base::Bind(&DeleteFilesOnFileThread, files_to_remove, reason)); +} + +void FileMonitorImpl::HardRecover(const InitCallback& callback) { + base::PostTaskAndReplyWithResult( + file_thread_task_runner_.get(), FROM_HERE, + base::Bind(&HardRecoverOnFileThread, download_file_dir_), callback); +} + +bool FileMonitorImpl::ReadyForCleanup(const Entry* entry) { + return entry->state == Entry::State::COMPLETE && + (base::Time::Now() - entry->completion_time) > file_keep_alive_time_; +} + +} // namespace download diff --git a/chromium/components/download/internal/file_monitor_impl.h b/chromium/components/download/internal/file_monitor_impl.h new file mode 100644 index 00000000000..f0e5b55ad51 --- /dev/null +++ b/chromium/components/download/internal/file_monitor_impl.h @@ -0,0 +1,63 @@ +// 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. + +#ifndef COMPONENTS_DOWNLOAD_INTERNAL_FILE_MONITOR_IMPL_H_ +#define COMPONENTS_DOWNLOAD_INTERNAL_FILE_MONITOR_IMPL_H_ + +#include "components/download/internal/file_monitor.h" + +#include <memory> +#include <set> +#include <string> +#include <vector> + +#include "base/files/file_path.h" +#include "base/macros.h" +#include "base/memory/ref_counted.h" +#include "base/memory/weak_ptr.h" +#include "base/sequenced_task_runner.h" +#include "components/download/internal/driver_entry.h" +#include "components/download/internal/model.h" +#include "components/download/internal/stats.h" + +namespace download { + +struct Entry; + +// An utility class containing various file cleanup methods. +class FileMonitorImpl : public FileMonitor { + public: + FileMonitorImpl( + const base::FilePath& download_file_dir, + const scoped_refptr<base::SequencedTaskRunner>& file_thread_task_runner, + base::TimeDelta file_keep_alive_time); + ~FileMonitorImpl() override; + + // FileMonitor implementation. + void Initialize(const InitCallback& callback) override; + void DeleteUnknownFiles( + const Model::EntryList& known_entries, + const std::vector<DriverEntry>& known_driver_entries) override; + std::vector<Entry*> CleanupFilesForCompletedEntries( + const Model::EntryList& entries, + const base::Closure& completion_callback) override; + void DeleteFiles(const std::set<base::FilePath>& files_to_remove, + stats::FileCleanupReason reason) override; + void HardRecover(const InitCallback& callback) override; + + private: + bool ReadyForCleanup(const Entry* entry); + + const base::FilePath download_file_dir_; + const base::TimeDelta file_keep_alive_time_; + + scoped_refptr<base::SequencedTaskRunner> file_thread_task_runner_; + base::WeakPtrFactory<FileMonitorImpl> weak_factory_; + + DISALLOW_COPY_AND_ASSIGN(FileMonitorImpl); +}; + +} // namespace download + +#endif // COMPONENTS_DOWNLOAD_INTERNAL_FILE_MONITOR_IMPL_H_ diff --git a/chromium/components/download/internal/file_monitor_unittest.cc b/chromium/components/download/internal/file_monitor_unittest.cc new file mode 100644 index 00000000000..d668872c802 --- /dev/null +++ b/chromium/components/download/internal/file_monitor_unittest.cc @@ -0,0 +1,173 @@ +// 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 <algorithm> + +#include "base/bind.h" +#include "base/callback.h" +#include "base/files/file_util.h" +#include "base/files/scoped_temp_dir.h" +#include "base/guid.h" +#include "base/memory/ptr_util.h" +#include "base/optional.h" +#include "base/test/test_simple_task_runner.h" +#include "base/threading/thread_task_runner_handle.h" +#include "components/download/internal/driver_entry.h" +#include "components/download/internal/entry.h" +#include "components/download/internal/file_monitor_impl.h" +#include "components/download/internal/test/entry_utils.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using testing::_; + +namespace download { + +class FileMonitorTest : public testing::Test { + public: + FileMonitorTest() + : task_runner_(new base::TestSimpleTaskRunner), + handle_(task_runner_), + completion_callback_called_(false) { + EXPECT_TRUE(scoped_temp_dir_.CreateUniqueTempDir()); + download_dir_ = scoped_temp_dir_.GetPath(); + base::TimeDelta keep_alive_time = base::TimeDelta::FromHours(12); + monitor_ = base::MakeUnique<FileMonitorImpl>(download_dir_, task_runner_, + keep_alive_time); + } + ~FileMonitorTest() override = default; + + void HardRecoveryResponse(bool result); + void CompletionCallback() { completion_callback_called_ = true; } + + protected: + base::FilePath CreateTemporaryFile(std::string file_name); + + base::ScopedTempDir scoped_temp_dir_; + scoped_refptr<base::TestSimpleTaskRunner> task_runner_; + base::ThreadTaskRunnerHandle handle_; + base::FilePath download_dir_; + bool completion_callback_called_; + std::unique_ptr<FileMonitor> monitor_; + + base::Optional<bool> hard_recovery_result_; + + private: + DISALLOW_COPY_AND_ASSIGN(FileMonitorTest); +}; + +base::FilePath FileMonitorTest::CreateTemporaryFile(std::string file_name) { + base::FilePath file_path = download_dir_.AppendASCII(file_name); + base::File file(file_path, base::File::FLAG_CREATE | base::File::FLAG_WRITE); + EXPECT_TRUE(file.IsValid()); + file.Close(); + + return file_path; +} + +void FileMonitorTest::HardRecoveryResponse(bool result) { + hard_recovery_result_ = result; +} + +TEST_F(FileMonitorTest, TestDeleteUnknownFiles) { + Entry entry1 = test::BuildEntry(DownloadClient::TEST, base::GenerateGUID()); + entry1.target_file_path = CreateTemporaryFile(entry1.guid); + + Entry entry2 = test::BuildEntry(DownloadClient::TEST, base::GenerateGUID()); + entry2.target_file_path = CreateTemporaryFile(entry2.guid); + + DriverEntry driver_entry1; + driver_entry1.guid = entry1.guid; + driver_entry1.current_file_path = entry1.target_file_path; + + DriverEntry driver_entry2; + driver_entry2.guid = base::GenerateGUID(); + driver_entry2.current_file_path = CreateTemporaryFile(driver_entry2.guid); + + base::FilePath temp_file1 = CreateTemporaryFile("temp1"); + base::FilePath temp_file2 = CreateTemporaryFile("temp2"); + + auto check_file_existence = [&](bool e1, bool e2, bool de1, bool de2, bool t1, + bool t2) { + EXPECT_EQ(e1, base::PathExists(entry1.target_file_path)); + EXPECT_EQ(e2, base::PathExists(entry2.target_file_path)); + EXPECT_EQ(de1, base::PathExists(driver_entry1.current_file_path)); + EXPECT_EQ(de2, base::PathExists(driver_entry2.current_file_path)); + EXPECT_EQ(t1, base::PathExists(temp_file1)); + EXPECT_EQ(t2, base::PathExists(temp_file2)); + }; + + check_file_existence(true, true, true, true, true, true); + + std::vector<Entry*> entries = {&entry1, &entry2}; + std::vector<DriverEntry> driver_entries = {driver_entry1, driver_entry2}; + + monitor_->DeleteUnknownFiles(entries, driver_entries); + task_runner_->RunUntilIdle(); + check_file_existence(true, true, true, true, false, false); + + entries = {&entry2}; + driver_entries = {driver_entry1, driver_entry2}; + monitor_->DeleteUnknownFiles(entries, driver_entries); + task_runner_->RunUntilIdle(); + check_file_existence(true, true, true, true, false, false); + + entries = {&entry2}; + driver_entries = {driver_entry2}; + monitor_->DeleteUnknownFiles(entries, driver_entries); + task_runner_->RunUntilIdle(); + check_file_existence(false, true, false, true, false, false); + + entries.clear(); + driver_entries.clear(); + monitor_->DeleteUnknownFiles(entries, driver_entries); + task_runner_->RunUntilIdle(); + check_file_existence(false, false, false, false, false, false); +} + +TEST_F(FileMonitorTest, TestCleanupFilesForCompletedEntries) { + Entry entry1 = test::BuildEntry(DownloadClient::TEST, base::GenerateGUID()); + entry1.state = Entry::State::COMPLETE; + entry1.completion_time = base::Time::Now() - base::TimeDelta::FromHours(20); + EXPECT_TRUE( + base::CreateTemporaryFileInDir(download_dir_, &entry1.target_file_path)); + + Entry entry2 = test::BuildEntry(DownloadClient::TEST, base::GenerateGUID()); + entry2.state = Entry::State::ACTIVE; + EXPECT_TRUE( + base::CreateTemporaryFileInDir(download_dir_, &entry2.target_file_path)); + + std::vector<Entry*> entries = {&entry1, &entry2}; + std::vector<Entry*> entries_to_remove = + monitor_->CleanupFilesForCompletedEntries( + entries, base::Bind(&FileMonitorTest::CompletionCallback, + base::Unretained(this))); + task_runner_->RunUntilIdle(); + + EXPECT_EQ(1u, entries_to_remove.size()); + EXPECT_FALSE(base::PathExists(entry1.target_file_path)); + EXPECT_TRUE(base::PathExists(entry2.target_file_path)); + EXPECT_TRUE(completion_callback_called_); +} + +TEST_F(FileMonitorTest, TestHardRecovery) { + base::FilePath temp_file1 = CreateTemporaryFile("temp1"); + base::FilePath temp_file2 = CreateTemporaryFile("temp2"); + + auto callback = base::Bind(&FileMonitorTest::HardRecoveryResponse, + base::Unretained(this)); + + EXPECT_TRUE(base::PathExists(temp_file1)); + EXPECT_TRUE(base::PathExists(temp_file2)); + + monitor_->HardRecover(callback); + task_runner_->RunUntilIdle(); + + EXPECT_TRUE(hard_recovery_result_.has_value()); + EXPECT_TRUE(hard_recovery_result_.value()); + EXPECT_FALSE(base::PathExists(temp_file1)); + EXPECT_FALSE(base::PathExists(temp_file2)); +} + +} // namespace download diff --git a/chromium/components/download/internal/model.h b/chromium/components/download/internal/model.h index ce5de88e450..c1b2ed0948f 100644 --- a/chromium/components/download/internal/model.h +++ b/chromium/components/download/internal/model.h @@ -9,11 +9,11 @@ #include <string> #include <vector> +#include "components/download/internal/entry.h" #include "components/download/public/clients.h" namespace download { -struct Entry; class Store; // The model that contains a runtime representation of Entry entries. Any @@ -30,12 +30,12 @@ class Model { // |success| is |false|, initialization of the Model and/or the underlying // Store failed. Initialization of the Model is complete after this // callback. If |success| is true it can be accessed now. - virtual void OnInitialized(bool success) = 0; + virtual void OnModelReady(bool success) = 0; - // Called asynchronously in response to a Model::Destroy call. If |success| - // is |false|, destruction of the Model and/or the underlying Store failed. - // Destruction of the Model is effectively complete after this callback. - virtual void OnDestroyed(bool success) = 0; + // Called asynchronously in response to a Model::HardRecover call. If + // |success| is |false|, recovery of the Model and/or the underlying Store + // failed. After this call there should be no entries stored in this Model. + virtual void OnModelHardRecoverComplete(bool success) = 0; // Called when an Entry addition is complete. |success| determines whether // or not the entry has been successfully persisted to the Store. @@ -62,10 +62,11 @@ class Model { // Initializes the Model. Client::OnInitialized() will be called in response. // The Model can be used after that call. - virtual void Initialize() = 0; + virtual void Initialize(Client* client) = 0; - // Destroys the Model. Client::OnDestroyed() will be called in response. - virtual void Destroy() = 0; + // Deletes and attempts to re-initialize the Store. + // Client::OnHardRecoveryComplete() will be called in response asynchronously. + virtual void HardRecover() = 0; // Adds |entry| to this Model and attempts to write |entry| to the Store. // Client::OnItemAdded() will be called in response asynchronously. diff --git a/chromium/components/download/internal/model_impl.cc b/chromium/components/download/internal/model_impl.cc index f061b5de07d..9a62961ab70 100644 --- a/chromium/components/download/internal/model_impl.cc +++ b/chromium/components/download/internal/model_impl.cc @@ -4,29 +4,37 @@ #include "components/download/internal/model_impl.h" +#include <map> + #include "base/bind.h" #include "base/memory/ptr_util.h" #include "components/download/internal/entry.h" +#include "components/download/internal/stats.h" namespace download { -ModelImpl::ModelImpl(Client* client, std::unique_ptr<Store> store) - : client_(client), store_(std::move(store)), weak_ptr_factory_(this) { - DCHECK(client_); +ModelImpl::ModelImpl(std::unique_ptr<Store> store) + : client_(nullptr), store_(std::move(store)), weak_ptr_factory_(this) { DCHECK(store_); } ModelImpl::~ModelImpl() = default; -void ModelImpl::Initialize() { +void ModelImpl::Initialize(Client* client) { + DCHECK(!client_); + client_ = client; + DCHECK(client_); + DCHECK(!store_->IsInitialized()); store_->Initialize(base::Bind(&ModelImpl::OnInitializedFinished, weak_ptr_factory_.GetWeakPtr())); } -void ModelImpl::Destroy() { - store_->Destroy(base::Bind(&ModelImpl::OnDestroyFinished, - weak_ptr_factory_.GetWeakPtr())); +void ModelImpl::HardRecover() { + entries_.clear(); + + store_->HardRecover(base::BindOnce(&ModelImpl::OnHardRecoverFinished, + weak_ptr_factory_.GetWeakPtr())); } void ModelImpl::Add(const Entry& entry) { @@ -35,19 +43,20 @@ void ModelImpl::Add(const Entry& entry) { entries_.emplace(entry.guid, base::MakeUnique<Entry>(entry)); - store_->Update(entry, base::Bind(&ModelImpl::OnAddFinished, - weak_ptr_factory_.GetWeakPtr(), entry.client, - entry.guid)); + store_->Update(entry, base::BindOnce(&ModelImpl::OnAddFinished, + weak_ptr_factory_.GetWeakPtr(), + entry.client, entry.guid)); } void ModelImpl::Update(const Entry& entry) { DCHECK(store_->IsInitialized()); DCHECK(entries_.find(entry.guid) != entries_.end()); - entries_[entry.guid] = base::MakeUnique<Entry>(entry); - store_->Update(entry, base::Bind(&ModelImpl::OnUpdateFinished, - weak_ptr_factory_.GetWeakPtr(), entry.client, - entry.guid)); + *entries_[entry.guid] = entry; + + store_->Update(entry, base::BindOnce(&ModelImpl::OnUpdateFinished, + weak_ptr_factory_.GetWeakPtr(), + entry.client, entry.guid)); } void ModelImpl::Remove(const std::string& guid) { @@ -56,11 +65,14 @@ void ModelImpl::Remove(const std::string& guid) { const auto& it = entries_.find(guid); DCHECK(it != entries_.end()); + // Pull out a separate guid and a DownloadClient so that when we destroy the + // entry we don't destroy the std::string that is backing the guid. + std::string standalone_guid = guid; DownloadClient client = it->second->client; entries_.erase(it); - store_->Remove(guid, - base::Bind(&ModelImpl::OnRemoveFinished, - weak_ptr_factory_.GetWeakPtr(), client, guid)); + store_->Remove(standalone_guid, base::BindOnce(&ModelImpl::OnRemoveFinished, + weak_ptr_factory_.GetWeakPtr(), + client, standalone_guid)); } Entry* ModelImpl::Get(const std::string& guid) { @@ -79,33 +91,41 @@ Model::EntryList ModelImpl::PeekEntries() { void ModelImpl::OnInitializedFinished( bool success, std::unique_ptr<std::vector<Entry>> entries) { + stats::LogModelOperationResult(stats::ModelAction::INITIALIZE, success); + if (!success) { - client_->OnInitialized(false); + client_->OnModelReady(false); return; } - for (const auto& entry : *entries) + std::map<Entry::State, uint32_t> entries_count; + for (const auto& entry : *entries) { + entries_count[entry.state]++; entries_.emplace(entry.guid, base::MakeUnique<Entry>(entry)); + } - client_->OnInitialized(true); + stats::LogEntries(entries_count); + client_->OnModelReady(true); } -void ModelImpl::OnDestroyFinished(bool success) { - store_.reset(); - entries_.clear(); - client_->OnDestroyed(success); +void ModelImpl::OnHardRecoverFinished(bool success) { + client_->OnModelHardRecoverComplete(success); } void ModelImpl::OnAddFinished(DownloadClient client, const std::string& guid, bool success) { + stats::LogModelOperationResult(stats::ModelAction::ADD, success); + // Don't notify the Client if the entry was already removed. - if (entries_.find(guid) == entries_.end()) + auto it = entries_.find(guid); + if (it == entries_.end()) return; // Remove the entry from the map if the add failed. - if (!success) - entries_.erase(guid); + if (!success) { + entries_.erase(it); + } client_->OnItemAdded(success, client, guid); } @@ -113,6 +133,8 @@ void ModelImpl::OnAddFinished(DownloadClient client, void ModelImpl::OnUpdateFinished(DownloadClient client, const std::string& guid, bool success) { + stats::LogModelOperationResult(stats::ModelAction::UPDATE, success); + // Don't notify the Client if the entry was already removed. if (entries_.find(guid) == entries_.end()) return; @@ -123,6 +145,8 @@ void ModelImpl::OnUpdateFinished(DownloadClient client, void ModelImpl::OnRemoveFinished(DownloadClient client, const std::string& guid, bool success) { + stats::LogModelOperationResult(stats::ModelAction::REMOVE, success); + DCHECK(entries_.find(guid) == entries_.end()); client_->OnItemRemoved(success, client, guid); } diff --git a/chromium/components/download/internal/model_impl.h b/chromium/components/download/internal/model_impl.h index 8c785019566..b2809f697f0 100644 --- a/chromium/components/download/internal/model_impl.h +++ b/chromium/components/download/internal/model_impl.h @@ -23,12 +23,12 @@ struct Entry; // The internal implementation of Model. class ModelImpl : public Model { public: - ModelImpl(Client* client, std::unique_ptr<Store> store); + ModelImpl(std::unique_ptr<Store> store); ~ModelImpl() override; // Model implementation. - void Initialize() override; - void Destroy() override; + void Initialize(Client* client) override; + void HardRecover() override; void Add(const Entry& entry) override; void Update(const Entry& entry) override; void Remove(const std::string& guid) override; @@ -40,7 +40,7 @@ class ModelImpl : public Model { void OnInitializedFinished(bool success, std::unique_ptr<std::vector<Entry>> entries); - void OnDestroyFinished(bool success); + void OnHardRecoverFinished(bool success); void OnAddFinished(DownloadClient client, const std::string& guid, bool success); @@ -53,7 +53,7 @@ class ModelImpl : public Model { // The external Model::Client reference that will receive all interesting // Model notifications. - Client* const client_; + Client* client_; // The backing Store that is responsible for saving and loading the // persisted entries. diff --git a/chromium/components/download/internal/model_impl_unittest.cc b/chromium/components/download/internal/model_impl_unittest.cc index 19a78391702..624715f7d25 100644 --- a/chromium/components/download/internal/model_impl_unittest.cc +++ b/chromium/components/download/internal/model_impl_unittest.cc @@ -11,7 +11,9 @@ #include "base/guid.h" #include "base/macros.h" #include "base/memory/ptr_util.h" +#include "base/test/histogram_tester.h" #include "components/download/internal/entry.h" +#include "components/download/internal/stats.h" #include "components/download/internal/test/entry_utils.h" #include "components/download/internal/test/mock_model_client.h" #include "components/download/internal/test/test_store.h" @@ -35,7 +37,7 @@ class DownloadServiceModelImplTest : public testing::Test { void SetUp() override { auto store = base::MakeUnique<test::TestStore>(); store_ = store.get(); - model_ = base::MakeUnique<ModelImpl>(&client_, std::move(store)); + model_ = base::MakeUnique<ModelImpl>(std::move(store)); } protected: @@ -51,85 +53,141 @@ class DownloadServiceModelImplTest : public testing::Test { TEST_F(DownloadServiceModelImplTest, SuccessfulLifecycle) { InSequence sequence; - EXPECT_CALL(client_, OnInitialized(true)).Times(1); - EXPECT_CALL(client_, OnDestroyed(true)).Times(1); + EXPECT_CALL(client_, OnModelReady(true)).Times(1); - model_->Initialize(); + model_->Initialize(&client_); EXPECT_TRUE(store_->init_called()); store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>()); - - model_->Destroy(); - EXPECT_TRUE(store_->destroy_called()); - store_->TriggerDestroy(true); } TEST_F(DownloadServiceModelImplTest, SuccessfulInitWithEntries) { - Entry entry1 = test::BuildEntry(DownloadClient::TEST, base::GenerateGUID()); - Entry entry2 = test::BuildEntry(DownloadClient::TEST, base::GenerateGUID()); + Entry entry1 = test::BuildBasicEntry(); + Entry entry2 = test::BuildBasicEntry(); std::vector<Entry> entries = {entry1, entry2}; + base::HistogramTester histogram_tester; InSequence sequence; - EXPECT_CALL(client_, OnInitialized(true)).Times(1); + EXPECT_CALL(client_, OnModelReady(true)).Times(1); - model_->Initialize(); + model_->Initialize(&client_); EXPECT_TRUE(store_->init_called()); store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>(entries)); - EXPECT_TRUE(test::SuperficialEntryCompare(&entry1, model_->Get(entry1.guid))); - EXPECT_TRUE(test::SuperficialEntryCompare(&entry2, model_->Get(entry2.guid))); + EXPECT_TRUE(test::CompareEntry(&entry1, model_->Get(entry1.guid))); + EXPECT_TRUE(test::CompareEntry(&entry2, model_->Get(entry2.guid))); + + // Verify histograms. + histogram_tester.ExpectBucketCount("Download.Service.Db.Records", 2, 1); + histogram_tester.ExpectBucketCount("Download.Service.Db.Records.New", 2, 1); + histogram_tester.ExpectBucketCount("Download.Service.Db.Records.Available", 0, + 1); + histogram_tester.ExpectBucketCount("Download.Service.Db.Records.Active", 0, + 1); + histogram_tester.ExpectBucketCount("Download.Service.Db.Records.Paused", 0, + 1); + histogram_tester.ExpectBucketCount("Download.Service.Db.Records.Complete", 0, + 1); } TEST_F(DownloadServiceModelImplTest, BadInit) { - EXPECT_CALL(client_, OnInitialized(false)).Times(1); + EXPECT_CALL(client_, OnModelReady(false)).Times(1); - model_->Initialize(); + model_->Initialize(&client_); EXPECT_TRUE(store_->init_called()); store_->TriggerInit(false, base::MakeUnique<std::vector<Entry>>()); } -TEST_F(DownloadServiceModelImplTest, BadDestroy) { - InSequence sequence; - EXPECT_CALL(client_, OnInitialized(true)).Times(1); - EXPECT_CALL(client_, OnDestroyed(false)).Times(1); +TEST_F(DownloadServiceModelImplTest, HardRecoverGoodModel) { + Entry entry1 = test::BuildBasicEntry(); + Entry entry2 = test::BuildBasicEntry(); + std::vector<Entry> entries = {entry1, entry2}; - model_->Initialize(); + EXPECT_CALL(client_, OnModelReady(true)).Times(1); + + model_->Initialize(&client_); EXPECT_TRUE(store_->init_called()); - store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>()); + store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>(entries)); + + EXPECT_CALL(client_, OnModelHardRecoverComplete(true)); - model_->Destroy(); - EXPECT_TRUE(store_->destroy_called()); - store_->TriggerDestroy(false); + model_->HardRecover(); + store_->TriggerHardRecover(true); + EXPECT_TRUE(model_->PeekEntries().empty()); +} + +TEST_F(DownloadServiceModelImplTest, HardRecoverBadModel) { + EXPECT_CALL(client_, OnModelReady(false)).Times(1); + + model_->Initialize(&client_); + EXPECT_TRUE(store_->init_called()); + store_->TriggerInit(false, base::MakeUnique<std::vector<Entry>>()); + + EXPECT_CALL(client_, OnModelHardRecoverComplete(true)); + + model_->HardRecover(); + store_->TriggerHardRecover(true); + EXPECT_TRUE(model_->PeekEntries().empty()); +} + +TEST_F(DownloadServiceModelImplTest, HardRecoverFailsGoodModel) { + Entry entry1 = test::BuildBasicEntry(); + Entry entry2 = test::BuildBasicEntry(); + std::vector<Entry> entries = {entry1, entry2}; + + EXPECT_CALL(client_, OnModelReady(true)).Times(1); + + model_->Initialize(&client_); + EXPECT_TRUE(store_->init_called()); + store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>(entries)); + + EXPECT_CALL(client_, OnModelHardRecoverComplete(false)); + + model_->HardRecover(); + store_->TriggerHardRecover(false); + EXPECT_TRUE(model_->PeekEntries().empty()); +} + +TEST_F(DownloadServiceModelImplTest, HardRecoverFailsBadModel) { + EXPECT_CALL(client_, OnModelReady(false)).Times(1); + + model_->Initialize(&client_); + EXPECT_TRUE(store_->init_called()); + store_->TriggerInit(false, base::MakeUnique<std::vector<Entry>>()); + + EXPECT_CALL(client_, OnModelHardRecoverComplete(false)); + + model_->HardRecover(); + store_->TriggerHardRecover(false); + EXPECT_TRUE(model_->PeekEntries().empty()); } TEST_F(DownloadServiceModelImplTest, Add) { - Entry entry1 = test::BuildEntry(DownloadClient::TEST, base::GenerateGUID()); - Entry entry2 = test::BuildEntry(DownloadClient::TEST, base::GenerateGUID()); + Entry entry1 = test::BuildBasicEntry(); + Entry entry2 = test::BuildBasicEntry(); InSequence sequence; - EXPECT_CALL(client_, OnInitialized(true)).Times(1); + EXPECT_CALL(client_, OnModelReady(true)).Times(1); EXPECT_CALL(client_, OnItemAdded(true, entry1.client, entry1.guid)).Times(1); EXPECT_CALL(client_, OnItemAdded(false, entry2.client, entry2.guid)).Times(1); - model_->Initialize(); + model_->Initialize(&client_); store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>()); model_->Add(entry1); - EXPECT_TRUE(test::SuperficialEntryCompare(&entry1, model_->Get(entry1.guid))); - EXPECT_TRUE( - test::SuperficialEntryCompare(&entry1, store_->LastUpdatedEntry())); + EXPECT_TRUE(test::CompareEntry(&entry1, model_->Get(entry1.guid))); + EXPECT_TRUE(test::CompareEntry(&entry1, store_->LastUpdatedEntry())); store_->TriggerUpdate(true); model_->Add(entry2); - EXPECT_TRUE(test::SuperficialEntryCompare(&entry2, model_->Get(entry2.guid))); - EXPECT_TRUE( - test::SuperficialEntryCompare(&entry2, store_->LastUpdatedEntry())); + EXPECT_TRUE(test::CompareEntry(&entry2, model_->Get(entry2.guid))); + EXPECT_TRUE(test::CompareEntry(&entry2, store_->LastUpdatedEntry())); store_->TriggerUpdate(false); EXPECT_EQ(nullptr, model_->Get(entry2.guid)); } TEST_F(DownloadServiceModelImplTest, Update) { - Entry entry1 = test::BuildEntry(DownloadClient::TEST, base::GenerateGUID()); + Entry entry1 = test::BuildBasicEntry(); Entry entry2(entry1); entry2.state = Entry::State::AVAILABLE; @@ -140,43 +198,51 @@ TEST_F(DownloadServiceModelImplTest, Update) { std::vector<Entry> entries = {entry1}; InSequence sequence; - EXPECT_CALL(client_, OnInitialized(true)).Times(1); + EXPECT_CALL(client_, OnModelReady(true)).Times(1); EXPECT_CALL(client_, OnItemUpdated(true, entry1.client, entry1.guid)) - .Times(1); + .Times(2); EXPECT_CALL(client_, OnItemUpdated(false, entry1.client, entry1.guid)) .Times(1); - model_->Initialize(); + model_->Initialize(&client_); store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>(entries)); + std::vector<Entry*> entries_pointers = model_->PeekEntries(); + // Update with a different object. model_->Update(entry2); - EXPECT_TRUE(test::SuperficialEntryCompare(&entry2, model_->Get(entry2.guid))); - EXPECT_TRUE( - test::SuperficialEntryCompare(&entry2, store_->LastUpdatedEntry())); + EXPECT_TRUE(test::CompareEntry(&entry2, model_->Get(entry2.guid))); + EXPECT_TRUE(test::CompareEntry(&entry2, store_->LastUpdatedEntry())); + store_->TriggerUpdate(true); + + // Update with the same object. + Entry* entry = model_->Get(entry1.guid); + entry->state = Entry::State::NEW; + model_->Update(*entry); store_->TriggerUpdate(true); + // Peek entries should return the same set of pointers. + EXPECT_TRUE(test::CompareEntryList(entries_pointers, model_->PeekEntries())); model_->Update(entry3); - EXPECT_TRUE(test::SuperficialEntryCompare(&entry3, model_->Get(entry3.guid))); - EXPECT_TRUE( - test::SuperficialEntryCompare(&entry3, store_->LastUpdatedEntry())); + EXPECT_TRUE(test::CompareEntry(&entry3, model_->Get(entry3.guid))); + EXPECT_TRUE(test::CompareEntry(&entry3, store_->LastUpdatedEntry())); store_->TriggerUpdate(false); - EXPECT_TRUE(test::SuperficialEntryCompare(&entry3, model_->Get(entry3.guid))); + EXPECT_TRUE(test::CompareEntry(&entry3, model_->Get(entry3.guid))); } TEST_F(DownloadServiceModelImplTest, Remove) { - Entry entry1 = test::BuildEntry(DownloadClient::TEST, base::GenerateGUID()); - Entry entry2 = test::BuildEntry(DownloadClient::TEST, base::GenerateGUID()); + Entry entry1 = test::BuildBasicEntry(); + Entry entry2 = test::BuildBasicEntry(); std::vector<Entry> entries = {entry1, entry2}; InSequence sequence; - EXPECT_CALL(client_, OnInitialized(true)).Times(1); + EXPECT_CALL(client_, OnModelReady(true)).Times(1); EXPECT_CALL(client_, OnItemRemoved(true, entry1.client, entry1.guid)) .Times(1); EXPECT_CALL(client_, OnItemRemoved(false, entry2.client, entry2.guid)) .Times(1); - model_->Initialize(); + model_->Initialize(&client_); store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>(entries)); model_->Remove(entry1.guid); @@ -191,50 +257,49 @@ TEST_F(DownloadServiceModelImplTest, Remove) { } TEST_F(DownloadServiceModelImplTest, Get) { - Entry entry = test::BuildEntry(DownloadClient::TEST, base::GenerateGUID()); + Entry entry = test::BuildBasicEntry(); std::vector<Entry> entries = {entry}; InSequence sequence; - EXPECT_CALL(client_, OnInitialized(true)).Times(1); + EXPECT_CALL(client_, OnModelReady(true)).Times(1); - model_->Initialize(); + model_->Initialize(&client_); store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>(entries)); - EXPECT_TRUE(test::SuperficialEntryCompare(&entry, model_->Get(entry.guid))); + EXPECT_TRUE(test::CompareEntry(&entry, model_->Get(entry.guid))); EXPECT_EQ(nullptr, model_->Get(base::GenerateGUID())); } TEST_F(DownloadServiceModelImplTest, PeekEntries) { - Entry entry1 = test::BuildEntry(DownloadClient::TEST, base::GenerateGUID()); - Entry entry2 = test::BuildEntry(DownloadClient::TEST, base::GenerateGUID()); + Entry entry1 = test::BuildBasicEntry(); + Entry entry2 = test::BuildBasicEntry(); std::vector<Entry> entries = {entry1, entry2}; InSequence sequence; - EXPECT_CALL(client_, OnInitialized(true)).Times(1); + EXPECT_CALL(client_, OnModelReady(true)).Times(1); - model_->Initialize(); + model_->Initialize(&client_); store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>(entries)); std::vector<Entry*> expected_peek = {&entry1, &entry2}; - EXPECT_TRUE( - test::SuperficialEntryListCompare(expected_peek, model_->PeekEntries())); + EXPECT_TRUE(test::CompareEntryList(expected_peek, model_->PeekEntries())); } TEST_F(DownloadServiceModelImplTest, TestRemoveAfterAdd) { - Entry entry = test::BuildEntry(DownloadClient::TEST, base::GenerateGUID()); + Entry entry = test::BuildBasicEntry(); InSequence sequence; - EXPECT_CALL(client_, OnInitialized(true)).Times(1); + EXPECT_CALL(client_, OnModelReady(true)).Times(1); EXPECT_CALL(client_, OnItemAdded(_, _, _)).Times(0); EXPECT_CALL(client_, OnItemRemoved(true, entry.client, entry.guid)).Times(1); - model_->Initialize(); + model_->Initialize(&client_); store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>()); model_->Add(entry); - EXPECT_TRUE(test::SuperficialEntryCompare(&entry, model_->Get(entry.guid))); + EXPECT_TRUE(test::CompareEntry(&entry, model_->Get(entry.guid))); model_->Remove(entry.guid); EXPECT_EQ(nullptr, model_->Get(entry.guid)); @@ -244,7 +309,7 @@ TEST_F(DownloadServiceModelImplTest, TestRemoveAfterAdd) { } TEST_F(DownloadServiceModelImplTest, TestRemoveAfterUpdate) { - Entry entry1 = test::BuildEntry(DownloadClient::TEST, base::GenerateGUID()); + Entry entry1 = test::BuildBasicEntry(); Entry entry2(entry1); entry2.state = Entry::State::AVAILABLE; @@ -252,17 +317,17 @@ TEST_F(DownloadServiceModelImplTest, TestRemoveAfterUpdate) { std::vector<Entry> entries = {entry1}; InSequence sequence; - EXPECT_CALL(client_, OnInitialized(true)).Times(1); + EXPECT_CALL(client_, OnModelReady(true)).Times(1); EXPECT_CALL(client_, OnItemUpdated(_, _, _)).Times(0); EXPECT_CALL(client_, OnItemRemoved(true, entry1.client, entry1.guid)) .Times(1); - model_->Initialize(); + model_->Initialize(&client_); store_->TriggerInit(true, base::MakeUnique<std::vector<Entry>>(entries)); - EXPECT_TRUE(test::SuperficialEntryCompare(&entry1, model_->Get(entry1.guid))); + EXPECT_TRUE(test::CompareEntry(&entry1, model_->Get(entry1.guid))); model_->Update(entry2); - EXPECT_TRUE(test::SuperficialEntryCompare(&entry2, model_->Get(entry2.guid))); + EXPECT_TRUE(test::CompareEntry(&entry2, model_->Get(entry2.guid))); model_->Remove(entry2.guid); EXPECT_EQ(nullptr, model_->Get(entry2.guid)); diff --git a/chromium/components/download/internal/noop_store.cc b/chromium/components/download/internal/noop_store.cc deleted file mode 100644 index 42785cbc5b1..00000000000 --- a/chromium/components/download/internal/noop_store.cc +++ /dev/null @@ -1,53 +0,0 @@ -// 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/noop_store.h" - -#include "base/bind.h" -#include "base/threading/thread_task_runner_handle.h" -#include "components/download/internal/entry.h" - -namespace download { - -NoopStore::NoopStore() : initialized_(false), weak_ptr_factory_(this) {} - -NoopStore::~NoopStore() = default; - -bool NoopStore::IsInitialized() { - return initialized_; -} - -void NoopStore::Initialize(InitCallback callback) { - DCHECK(!IsInitialized()); - - base::ThreadTaskRunnerHandle::Get()->PostTask( - FROM_HERE, - base::BindOnce(&NoopStore::OnInitFinished, weak_ptr_factory_.GetWeakPtr(), - std::move(callback))); -} - -void NoopStore::Destroy(StoreCallback callback) { - base::ThreadTaskRunnerHandle::Get()->PostTask( - FROM_HERE, base::BindOnce(std::move(callback), true /** success */)); -} - -void NoopStore::Update(const Entry& entry, StoreCallback callback) { - base::ThreadTaskRunnerHandle::Get()->PostTask( - FROM_HERE, base::BindOnce(std::move(callback), true /** success */)); -} - -void NoopStore::Remove(const std::string& guid, StoreCallback callback) { - base::ThreadTaskRunnerHandle::Get()->PostTask( - FROM_HERE, base::BindOnce(std::move(callback), true /** success */)); -} - -void NoopStore::OnInitFinished(InitCallback callback) { - initialized_ = true; - - std::unique_ptr<std::vector<Entry>> entries = - base::MakeUnique<std::vector<Entry>>(); - std::move(callback).Run(true /** success */, std::move(entries)); -} - -} // namespace download diff --git a/chromium/components/download/internal/noop_store.h b/chromium/components/download/internal/noop_store.h deleted file mode 100644 index 4bad9ed2e13..00000000000 --- a/chromium/components/download/internal/noop_store.h +++ /dev/null @@ -1,46 +0,0 @@ -// 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. - -#ifndef COMPONENTS_DOWNLOAD_INTERNAL_NOOP_STORE_H_ -#define COMPONENTS_DOWNLOAD_INTERNAL_NOOP_STORE_H_ - -#include "base/macros.h" -#include "base/memory/weak_ptr.h" -#include "components/download/internal/store.h" - -namespace download { - -struct Entry; - -// A Store implementation that doesn't do anything but honors the interface -// requirements. -// TODO(dtrainor, shaktisahu): Remove this if it's no longer necessary after the -// real Store implementation is added. -class NoopStore : public Store { - public: - NoopStore(); - ~NoopStore() override; - - // Store implementation. - bool IsInitialized() override; - void Initialize(InitCallback callback) override; - void Destroy(StoreCallback callback) override; - void Update(const Entry& entry, StoreCallback callback) override; - void Remove(const std::string& guid, StoreCallback callback) override; - - private: - void OnInitFinished(InitCallback callback); - - // Whether or not this Store is 'initialized.' Just gets set to |true| once - // Initialize() is called. - bool initialized_; - - base::WeakPtrFactory<NoopStore> weak_ptr_factory_; - - DISALLOW_COPY_AND_ASSIGN(NoopStore); -}; - -} // namespace download - -#endif // COMPONENTS_DOWNLOAD_INTERNAL_NOOP_STORE_H_ diff --git a/chromium/components/download/internal/proto/BUILD.gn b/chromium/components/download/internal/proto/BUILD.gn new file mode 100644 index 00000000000..9d6c33e751f --- /dev/null +++ b/chromium/components/download/internal/proto/BUILD.gn @@ -0,0 +1,18 @@ +# 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. + +import("//third_party/protobuf/proto_library.gni") + +proto_library("proto") { + visibility = [ + "//components/download/content/factory", + "//components/download/internal:*", + ] + + sources = [ + "entry.proto", + "request.proto", + "scheduling.proto", + ] +} diff --git a/chromium/components/download/internal/proto/entry.proto b/chromium/components/download/internal/proto/entry.proto new file mode 100644 index 00000000000..426588a29e6 --- /dev/null +++ b/chromium/components/download/internal/proto/entry.proto @@ -0,0 +1,55 @@ +// 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. + +syntax = "proto2"; + +option optimize_for = LITE_RUNTIME; + +package protodb; + +import "request.proto"; +import "scheduling.proto"; + +// This should stay in sync with the DownloadClient enum +// (components/download/public/clients.h). +enum DownloadClient { + INVALID = 0; + TEST = 1; + TEST_2 = 2; + TEST_3 = 3; + OFFLINE_PAGE_PREFETCH = 4; + BOUNDARY = 5; +} + +// Stores the request params, internal state, metrics and metadata associated +// with a download request. +message Entry { + // This should stay in sync with the State enum + // (components/download/internal/entry.h). + enum State { + NEW = 0; + AVAILABLE = 1; + ACTIVE = 2; + PAUSED = 3; + COMPLETE = 4; + } + + // Identification Parameters. + optional DownloadClient name_space = 1; + optional string guid = 2; + + // Requested Parameters. + optional SchedulingParams scheduling_params = 3; + optional RequestParams request_params = 4; + + // Internal Tracking States. + optional State state = 5; + optional string target_file_path = 6; + + // Uses internal time representation. + optional int64 create_time = 7; + optional int64 completion_time = 8; + + optional uint32 attempt_count = 9; +} diff --git a/chromium/components/download/internal/proto/request.proto b/chromium/components/download/internal/proto/request.proto new file mode 100644 index 00000000000..173c6856182 --- /dev/null +++ b/chromium/components/download/internal/proto/request.proto @@ -0,0 +1,21 @@ +// 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. + +syntax = "proto2"; + +option optimize_for = LITE_RUNTIME; + +package protodb; + +message RequestHeader { + optional string key = 1; + optional string value = 2; +} + +// Stores the HTTP request params associated with a download request. +message RequestParams { + optional string url = 1; + optional string method = 2; + repeated RequestHeader headers = 3; +} diff --git a/chromium/components/download/internal/proto/scheduling.proto b/chromium/components/download/internal/proto/scheduling.proto new file mode 100644 index 00000000000..2b8cc7ac747 --- /dev/null +++ b/chromium/components/download/internal/proto/scheduling.proto @@ -0,0 +1,43 @@ +// 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. + +syntax = "proto2"; + +option optimize_for = LITE_RUNTIME; + +package protodb; + +// Stores the scheduling params associated with a download request. +message SchedulingParams { + // This should stay in sync with the NetworkRequirements enum + // (components/download/public/download_params.h). + enum NetworkRequirements { + NONE = 0; + OPTIMISTIC = 1; + UNMETERED = 2; + } + + // This should stay in sync with the BatteryRequirements enum + // (components/download/public/download_params.h). + enum BatteryRequirements { + BATTERY_INSENSITIVE = 0; + BATTERY_SENSITIVE = 1; + } + + // This should stay in sync with the Priority enum + // (components/download/public/download_params.h). + enum Priority { + LOW = 0; + NORMAL = 1; + HIGH = 2; + UI = 3; + } + + // Uses internal time representation. + optional int64 cancel_time = 2; + + optional Priority priority = 3; + optional NetworkRequirements network_requirements = 4; + optional BatteryRequirements battery_requirements = 5; +} diff --git a/chromium/components/download/internal/proto_conversions.cc b/chromium/components/download/internal/proto_conversions.cc new file mode 100644 index 00000000000..f78f521b054 --- /dev/null +++ b/chromium/components/download/internal/proto_conversions.cc @@ -0,0 +1,307 @@ +// 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 <utility> + +#include "base/memory/ptr_util.h" +#include "base/time/time.h" +#include "components/download/internal/proto_conversions.h" +#include "net/http/http_request_headers.h" + +namespace download { + +protodb::Entry_State ProtoConversions::RequestStateToProto(Entry::State state) { + switch (state) { + case Entry::State::NEW: + return protodb::Entry_State_NEW; + case Entry::State::AVAILABLE: + return protodb::Entry_State_AVAILABLE; + case Entry::State::ACTIVE: + return protodb::Entry_State_ACTIVE; + case Entry::State::PAUSED: + return protodb::Entry_State_PAUSED; + case Entry::State::COMPLETE: + return protodb::Entry_State_COMPLETE; + case Entry::State::COUNT: + break; + } + + NOTREACHED(); + return protodb::Entry_State_NEW; +} + +Entry::State ProtoConversions::RequestStateFromProto( + protodb::Entry_State state) { + switch (state) { + case protodb::Entry_State_NEW: + return Entry::State::NEW; + case protodb::Entry_State_AVAILABLE: + return Entry::State::AVAILABLE; + case protodb::Entry_State_ACTIVE: + return Entry::State::ACTIVE; + case protodb::Entry_State_PAUSED: + return Entry::State::PAUSED; + case protodb::Entry_State_COMPLETE: + return Entry::State::COMPLETE; + } + + NOTREACHED(); + return Entry::State::NEW; +} + +protodb::DownloadClient ProtoConversions::DownloadClientToProto( + DownloadClient client) { + switch (client) { + case DownloadClient::INVALID: + return protodb::DownloadClient::INVALID; + case DownloadClient::TEST: + return protodb::DownloadClient::TEST; + case DownloadClient::TEST_2: + return protodb::DownloadClient::TEST_2; + case DownloadClient::TEST_3: + return protodb::DownloadClient::TEST_3; + case DownloadClient::OFFLINE_PAGE_PREFETCH: + return protodb::DownloadClient::OFFLINE_PAGE_PREFETCH; + case DownloadClient::BOUNDARY: + return protodb::DownloadClient::BOUNDARY; + } + + NOTREACHED(); + return protodb::DownloadClient::INVALID; +} + +DownloadClient ProtoConversions::DownloadClientFromProto( + protodb::DownloadClient client) { + switch (client) { + case protodb::DownloadClient::INVALID: + return DownloadClient::INVALID; + case protodb::DownloadClient::TEST: + return DownloadClient::TEST; + case protodb::DownloadClient::TEST_2: + return DownloadClient::TEST_2; + case protodb::DownloadClient::TEST_3: + return DownloadClient::TEST_3; + case protodb::DownloadClient::OFFLINE_PAGE_PREFETCH: + return DownloadClient::OFFLINE_PAGE_PREFETCH; + case protodb::DownloadClient::BOUNDARY: + return DownloadClient::BOUNDARY; + } + + NOTREACHED(); + return DownloadClient::INVALID; +} + +SchedulingParams::NetworkRequirements +ProtoConversions::NetworkRequirementsFromProto( + protodb::SchedulingParams_NetworkRequirements network_requirements) { + switch (network_requirements) { + case protodb::SchedulingParams_NetworkRequirements_NONE: + return SchedulingParams::NetworkRequirements::NONE; + case protodb::SchedulingParams_NetworkRequirements_OPTIMISTIC: + return SchedulingParams::NetworkRequirements::OPTIMISTIC; + case protodb::SchedulingParams_NetworkRequirements_UNMETERED: + return SchedulingParams::NetworkRequirements::UNMETERED; + } + + NOTREACHED(); + return SchedulingParams::NetworkRequirements::NONE; +} + +protodb::SchedulingParams_NetworkRequirements +ProtoConversions::NetworkRequirementsToProto( + SchedulingParams::NetworkRequirements network_requirements) { + switch (network_requirements) { + case SchedulingParams::NetworkRequirements::NONE: + return protodb::SchedulingParams_NetworkRequirements_NONE; + case SchedulingParams::NetworkRequirements::OPTIMISTIC: + return protodb::SchedulingParams_NetworkRequirements_OPTIMISTIC; + case SchedulingParams::NetworkRequirements::UNMETERED: + return protodb::SchedulingParams_NetworkRequirements_UNMETERED; + case SchedulingParams::NetworkRequirements::COUNT: + break; + } + + NOTREACHED(); + return protodb::SchedulingParams_NetworkRequirements_NONE; +} + +SchedulingParams::BatteryRequirements +ProtoConversions::BatteryRequirementsFromProto( + protodb::SchedulingParams_BatteryRequirements battery_requirements) { + switch (battery_requirements) { + case protodb::SchedulingParams_BatteryRequirements_BATTERY_INSENSITIVE: + return SchedulingParams::BatteryRequirements::BATTERY_INSENSITIVE; + case protodb::SchedulingParams_BatteryRequirements_BATTERY_SENSITIVE: + return SchedulingParams::BatteryRequirements::BATTERY_SENSITIVE; + } + + NOTREACHED(); + return SchedulingParams::BatteryRequirements::BATTERY_INSENSITIVE; +} + +protodb::SchedulingParams_BatteryRequirements +ProtoConversions::BatteryRequirementsToProto( + SchedulingParams::BatteryRequirements battery_requirements) { + switch (battery_requirements) { + case SchedulingParams::BatteryRequirements::BATTERY_INSENSITIVE: + return protodb::SchedulingParams_BatteryRequirements_BATTERY_INSENSITIVE; + case SchedulingParams::BatteryRequirements::BATTERY_SENSITIVE: + return protodb::SchedulingParams_BatteryRequirements_BATTERY_SENSITIVE; + case SchedulingParams::BatteryRequirements::COUNT: + break; + } + + NOTREACHED(); + return protodb::SchedulingParams_BatteryRequirements_BATTERY_INSENSITIVE; +} + +SchedulingParams::Priority ProtoConversions::SchedulingPriorityFromProto( + protodb::SchedulingParams_Priority priority) { + switch (priority) { + case protodb::SchedulingParams_Priority_LOW: + return SchedulingParams::Priority::LOW; + case protodb::SchedulingParams_Priority_NORMAL: + return SchedulingParams::Priority::NORMAL; + case protodb::SchedulingParams_Priority_HIGH: + return SchedulingParams::Priority::HIGH; + case protodb::SchedulingParams_Priority_UI: + return SchedulingParams::Priority::UI; + } + + NOTREACHED(); + return SchedulingParams::Priority::LOW; +} + +protodb::SchedulingParams_Priority ProtoConversions::SchedulingPriorityToProto( + SchedulingParams::Priority priority) { + switch (priority) { + case SchedulingParams::Priority::LOW: + return protodb::SchedulingParams_Priority_LOW; + case SchedulingParams::Priority::NORMAL: + return protodb::SchedulingParams_Priority_NORMAL; + case SchedulingParams::Priority::HIGH: + return protodb::SchedulingParams_Priority_HIGH; + case SchedulingParams::Priority::UI: + return protodb::SchedulingParams_Priority_UI; + case SchedulingParams::Priority::COUNT: + break; + } + + NOTREACHED(); + return protodb::SchedulingParams_Priority_LOW; +} + +SchedulingParams ProtoConversions::SchedulingParamsFromProto( + const protodb::SchedulingParams& proto) { + SchedulingParams scheduling_params; + + scheduling_params.cancel_time = + base::Time::FromInternalValue(proto.cancel_time()); + scheduling_params.priority = SchedulingPriorityFromProto(proto.priority()); + scheduling_params.network_requirements = + NetworkRequirementsFromProto(proto.network_requirements()); + scheduling_params.battery_requirements = + BatteryRequirementsFromProto(proto.battery_requirements()); + + return scheduling_params; +} + +void ProtoConversions::SchedulingParamsToProto( + const SchedulingParams& scheduling_params, + protodb::SchedulingParams* proto) { + proto->set_cancel_time(scheduling_params.cancel_time.ToInternalValue()); + proto->set_priority(SchedulingPriorityToProto(scheduling_params.priority)); + proto->set_network_requirements( + NetworkRequirementsToProto(scheduling_params.network_requirements)); + proto->set_battery_requirements( + BatteryRequirementsToProto(scheduling_params.battery_requirements)); +} + +RequestParams ProtoConversions::RequestParamsFromProto( + const protodb::RequestParams& proto) { + RequestParams request_params; + request_params.url = GURL(proto.url()); + request_params.method = proto.method(); + + for (int i = 0; i < proto.headers_size(); i++) { + protodb::RequestHeader header = proto.headers(i); + request_params.request_headers.SetHeader(header.key(), header.value()); + } + + return request_params; +} + +void ProtoConversions::RequestParamsToProto(const RequestParams& request_params, + protodb::RequestParams* proto) { + proto->set_url(request_params.url.spec()); + proto->set_method(request_params.method); + + int i = 0; + net::HttpRequestHeaders::Iterator iter(request_params.request_headers); + while (iter.GetNext()) { + protodb::RequestHeader* header = proto->add_headers(); + header->set_key(iter.name()); + header->set_value(iter.value()); + i++; + } +} + +Entry ProtoConversions::EntryFromProto(const protodb::Entry& proto) { + Entry entry; + + entry.guid = proto.guid(); + entry.client = DownloadClientFromProto(proto.name_space()); + entry.scheduling_params = + SchedulingParamsFromProto(proto.scheduling_params()); + entry.request_params = RequestParamsFromProto(proto.request_params()); + entry.state = RequestStateFromProto(proto.state()); + entry.target_file_path = + base::FilePath::FromUTF8Unsafe(proto.target_file_path()); + entry.create_time = base::Time::FromInternalValue(proto.create_time()); + entry.completion_time = + base::Time::FromInternalValue(proto.completion_time()); + entry.attempt_count = proto.attempt_count(); + + return entry; +} + +protodb::Entry ProtoConversions::EntryToProto(const Entry& entry) { + protodb::Entry proto; + + proto.set_guid(entry.guid); + proto.set_name_space(DownloadClientToProto(entry.client)); + SchedulingParamsToProto(entry.scheduling_params, + proto.mutable_scheduling_params()); + RequestParamsToProto(entry.request_params, proto.mutable_request_params()); + proto.set_state(RequestStateToProto(entry.state)); + proto.set_target_file_path(entry.target_file_path.AsUTF8Unsafe()); + proto.set_create_time(entry.create_time.ToInternalValue()); + proto.set_completion_time(entry.completion_time.ToInternalValue()); + proto.set_attempt_count(entry.attempt_count); + + return proto; +} + +std::unique_ptr<std::vector<Entry>> ProtoConversions::EntryVectorFromProto( + std::unique_ptr<std::vector<protodb::Entry>> protos) { + auto entries = base::MakeUnique<std::vector<Entry>>(); + for (auto& proto : *protos) { + entries->push_back(EntryFromProto(proto)); + } + + return entries; +} + +std::unique_ptr<std::vector<protodb::Entry>> +ProtoConversions::EntryVectorToProto( + std::unique_ptr<std::vector<Entry>> entries) { + auto protos = base::MakeUnique<std::vector<protodb::Entry>>(); + for (auto& entry : *entries) { + protos->push_back(EntryToProto(entry)); + } + + return protos; +} + +} // namespace download diff --git a/chromium/components/download/internal/proto_conversions.h b/chromium/components/download/internal/proto_conversions.h new file mode 100644 index 00000000000..4d2e4e53661 --- /dev/null +++ b/chromium/components/download/internal/proto_conversions.h @@ -0,0 +1,64 @@ +// 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. + +#ifndef COMPONENTS_DOWNLOAD_INTERNAL_PROTO_CONVERSIONS_H_ +#define COMPONENTS_DOWNLOAD_INTERNAL_PROTO_CONVERSIONS_H_ + +#include "components/download/internal/entry.h" +#include "components/download/internal/proto/entry.pb.h" +#include "components/download/internal/proto/request.pb.h" +#include "components/download/internal/proto/scheduling.pb.h" + +namespace download { + +class ProtoConversions { + public: + static Entry EntryFromProto(const protodb::Entry& proto); + + static protodb::Entry EntryToProto(const Entry& entry); + + static std::unique_ptr<std::vector<Entry>> EntryVectorFromProto( + std::unique_ptr<std::vector<protodb::Entry>> proto); + + static std::unique_ptr<std::vector<protodb::Entry>> EntryVectorToProto( + std::unique_ptr<std::vector<Entry>> entries); + + protected: + static protodb::Entry_State RequestStateToProto(Entry::State state); + static Entry::State RequestStateFromProto(protodb::Entry_State state); + + static protodb::DownloadClient DownloadClientToProto(DownloadClient client); + static DownloadClient DownloadClientFromProto(protodb::DownloadClient client); + + static SchedulingParams::NetworkRequirements NetworkRequirementsFromProto( + protodb::SchedulingParams_NetworkRequirements network_requirements); + static protodb::SchedulingParams_NetworkRequirements + NetworkRequirementsToProto( + SchedulingParams::NetworkRequirements network_requirements); + + static SchedulingParams::BatteryRequirements BatteryRequirementsFromProto( + protodb::SchedulingParams_BatteryRequirements battery_requirements); + static protodb::SchedulingParams_BatteryRequirements + BatteryRequirementsToProto( + SchedulingParams::BatteryRequirements battery_requirements); + + static SchedulingParams::Priority SchedulingPriorityFromProto( + protodb::SchedulingParams_Priority priority); + static protodb::SchedulingParams_Priority SchedulingPriorityToProto( + SchedulingParams::Priority priority); + + static SchedulingParams SchedulingParamsFromProto( + const protodb::SchedulingParams& proto); + static void SchedulingParamsToProto(const SchedulingParams& scheduling_params, + protodb::SchedulingParams* proto); + + static RequestParams RequestParamsFromProto( + const protodb::RequestParams& proto); + static void RequestParamsToProto(const RequestParams& request_params, + protodb::RequestParams* proto); +}; + +} // namespace download + +#endif // COMPONENTS_DOWNLOAD_INTERNAL_PROTO_CONVERSIONS_H_ diff --git a/chromium/components/download/internal/proto_conversions_unittest.cc b/chromium/components/download/internal/proto_conversions_unittest.cc new file mode 100644 index 00000000000..ea1e9cbf4fd --- /dev/null +++ b/chromium/components/download/internal/proto_conversions_unittest.cc @@ -0,0 +1,153 @@ +// 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 <utility> + +#include "base/guid.h" +#include "base/memory/ptr_util.h" +#include "components/download/internal/entry.h" +#include "components/download/internal/proto_conversions.h" +#include "components/download/internal/test/entry_utils.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { +std::string TEST_URL = "https://google.com"; + +} // namespace + +namespace download { + +class ProtoConversionsTest : public testing::Test, public ProtoConversions { + public: + ~ProtoConversionsTest() override {} +}; + +TEST_F(ProtoConversionsTest, StateConversion) { + Entry::State states[] = {Entry::State::NEW, Entry::State::AVAILABLE, + Entry::State::ACTIVE, Entry::State::PAUSED, + Entry::State::COMPLETE}; + for (auto state : states) { + ASSERT_EQ(state, RequestStateFromProto(RequestStateToProto(state))); + } +} + +TEST_F(ProtoConversionsTest, DownloadClientConversion) { + DownloadClient clients[] = {DownloadClient::INVALID, DownloadClient::TEST, + DownloadClient::TEST_2, DownloadClient::BOUNDARY}; + for (auto client : clients) { + ASSERT_EQ(client, DownloadClientFromProto(DownloadClientToProto(client))); + } +} + +TEST_F(ProtoConversionsTest, NetworkRequirementsConversion) { + SchedulingParams::NetworkRequirements values[] = { + SchedulingParams::NetworkRequirements::NONE, + SchedulingParams::NetworkRequirements::OPTIMISTIC, + SchedulingParams::NetworkRequirements::UNMETERED}; + for (auto value : values) { + ASSERT_EQ(value, + NetworkRequirementsFromProto(NetworkRequirementsToProto(value))); + } +} + +TEST_F(ProtoConversionsTest, BatteryRequirementsConversion) { + SchedulingParams::BatteryRequirements values[] = { + SchedulingParams::BatteryRequirements::BATTERY_INSENSITIVE, + SchedulingParams::BatteryRequirements::BATTERY_SENSITIVE}; + for (auto value : values) { + ASSERT_EQ(value, + BatteryRequirementsFromProto(BatteryRequirementsToProto(value))); + } +} + +TEST_F(ProtoConversionsTest, SchedulingPriorityConversion) { + SchedulingParams::Priority values[] = { + SchedulingParams::Priority::LOW, SchedulingParams::Priority::NORMAL, + SchedulingParams::Priority::HIGH, SchedulingParams::Priority::UI, + SchedulingParams::Priority::DEFAULT}; + for (auto value : values) { + ASSERT_EQ(value, + SchedulingPriorityFromProto(SchedulingPriorityToProto(value))); + } +} + +TEST_F(ProtoConversionsTest, SchedulingParamsConversion) { + SchedulingParams expected; + expected.cancel_time = base::Time::Now(); + expected.priority = SchedulingParams::Priority::DEFAULT; + expected.network_requirements = + SchedulingParams::NetworkRequirements::OPTIMISTIC; + expected.battery_requirements = + SchedulingParams::BatteryRequirements::BATTERY_SENSITIVE; + + protodb::SchedulingParams proto; + SchedulingParamsToProto(expected, &proto); + SchedulingParams actual = SchedulingParamsFromProto(proto); + EXPECT_EQ(expected.cancel_time, actual.cancel_time); + EXPECT_EQ(SchedulingParams::Priority::DEFAULT, actual.priority); + EXPECT_EQ(SchedulingParams::NetworkRequirements::OPTIMISTIC, + actual.network_requirements); + EXPECT_EQ(SchedulingParams::BatteryRequirements::BATTERY_SENSITIVE, + actual.battery_requirements); +} + +TEST_F(ProtoConversionsTest, RequestParamsWithHeadersConversion) { + RequestParams expected; + expected.url = GURL(TEST_URL); + expected.method = "GET"; + expected.request_headers.SetHeader("key1", "value1"); + expected.request_headers.SetHeader("key2", "value2"); + + protodb::RequestParams proto; + RequestParamsToProto(expected, &proto); + RequestParams actual = RequestParamsFromProto(proto); + + EXPECT_EQ(expected.url, actual.url); + EXPECT_EQ(expected.method, actual.method); + + std::string out; + actual.request_headers.GetHeader("key1", &out); + EXPECT_EQ("value1", out); + actual.request_headers.GetHeader("key2", &out); + EXPECT_EQ("value2", out); + EXPECT_EQ(expected.request_headers.ToString(), + actual.request_headers.ToString()); +} + +TEST_F(ProtoConversionsTest, EntryConversion) { + Entry expected = test::BuildEntry(DownloadClient::TEST, base::GenerateGUID()); + Entry actual = EntryFromProto(EntryToProto(expected)); + EXPECT_TRUE(test::CompareEntry(&expected, &actual)); + + expected = test::BuildEntry( + DownloadClient::TEST, base::GenerateGUID(), base::Time::Now(), + SchedulingParams::NetworkRequirements::OPTIMISTIC, + SchedulingParams::BatteryRequirements::BATTERY_SENSITIVE, + SchedulingParams::Priority::HIGH, GURL(TEST_URL), "GET", + Entry::State::ACTIVE, base::FilePath(FILE_PATH_LITERAL("/test/xyz")), + base::Time::Now(), base::Time::Now(), 3); + actual = EntryFromProto(EntryToProto(expected)); + EXPECT_TRUE(test::CompareEntry(&expected, &actual)); +} + +TEST_F(ProtoConversionsTest, EntryVectorConversion) { + std::vector<Entry> expected; + expected.push_back( + test::BuildEntry(DownloadClient::TEST, base::GenerateGUID())); + expected.push_back( + test::BuildEntry(DownloadClient::TEST_2, base::GenerateGUID())); + expected.push_back(test::BuildEntry( + DownloadClient::TEST, base::GenerateGUID(), base::Time::Now(), + SchedulingParams::NetworkRequirements::OPTIMISTIC, + SchedulingParams::BatteryRequirements::BATTERY_SENSITIVE, + SchedulingParams::Priority::HIGH, GURL(TEST_URL), "GET", + Entry::State::ACTIVE, base::FilePath(FILE_PATH_LITERAL("/test/xyz")), + base::Time::Now(), base::Time::Now(), 2)); + + auto actual = EntryVectorFromProto( + EntryVectorToProto(base::MakeUnique<std::vector<Entry>>(expected))); + EXPECT_TRUE(test::CompareEntryList(expected, *actual)); +} + +} // namespace download diff --git a/chromium/components/download/internal/scheduler/battery_listener.cc b/chromium/components/download/internal/scheduler/battery_listener.cc deleted file mode 100644 index b015ca15ffd..00000000000 --- a/chromium/components/download/internal/scheduler/battery_listener.cc +++ /dev/null @@ -1,58 +0,0 @@ -// 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/scheduler/battery_listener.h" - -#include "base/power_monitor/power_monitor.h" - -namespace download { - -// Helper function that converts the battery status to battery requirement. -SchedulingParams::BatteryRequirements ToBatteryRequirement( - bool on_battery_power) { - return on_battery_power - ? SchedulingParams::BatteryRequirements::BATTERY_INSENSITIVE - : SchedulingParams::BatteryRequirements::BATTERY_SENSITIVE; -} - -BatteryListener::BatteryListener() = default; - -BatteryListener::~BatteryListener() { - Stop(); -} - -SchedulingParams::BatteryRequirements BatteryListener::CurrentBatteryStatus() - const { - return ToBatteryRequirement(base::PowerMonitor::Get()->IsOnBatteryPower()); -} - -void BatteryListener::Start() { - base::PowerMonitor* monitor = base::PowerMonitor::Get(); - DCHECK(monitor); - monitor->AddObserver(this); -} - -void BatteryListener::Stop() { - base::PowerMonitor::Get()->RemoveObserver(this); -} - -void BatteryListener::AddObserver(Observer* observer) { - observers_.AddObserver(observer); -} - -void BatteryListener::RemoveObserver(Observer* observer) { - observers_.RemoveObserver(observer); -} - -void BatteryListener::OnPowerStateChange(bool on_battery_power) { - NotifyBatteryChange(ToBatteryRequirement(on_battery_power)); -} - -void BatteryListener::NotifyBatteryChange( - SchedulingParams::BatteryRequirements current_battery) { - for (auto& observer : observers_) - observer.OnBatteryChange(current_battery); -} - -} // namespace download diff --git a/chromium/components/download/internal/scheduler/battery_listener.h b/chromium/components/download/internal/scheduler/battery_listener.h deleted file mode 100644 index 2ddc6109b55..00000000000 --- a/chromium/components/download/internal/scheduler/battery_listener.h +++ /dev/null @@ -1,59 +0,0 @@ -// 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. - -#ifndef COMPONENTS_DOWNLOAD_INTERNAL_SCHEDULER_BATTERY_LISTENER_H_ -#define COMPONENTS_DOWNLOAD_INTERNAL_SCHEDULER_BATTERY_LISTENER_H_ - -#include <memory> - -#include "base/observer_list.h" -#include "base/power_monitor/power_observer.h" -#include "components/download/public/download_params.h" - -namespace download { - -// Listen to battery charing state changes and notify observers in download -// service. -// TODO(xingliu): Converts to device service when it's fully done. -class BatteryListener : public base::PowerObserver { - public: - class Observer { - public: - // Called when certain battery requirements are meet. - virtual void OnBatteryChange( - SchedulingParams::BatteryRequirements battery_status) = 0; - }; - - BatteryListener(); - ~BatteryListener() override; - - // Retrieves the current minimum battery requirement. - SchedulingParams::BatteryRequirements CurrentBatteryStatus() const; - - // Start to listen to battery change. - void Start(); - - // Stop to listen to battery change. - void Stop(); - - // Adds/Removes observers. - void AddObserver(Observer* observer); - void RemoveObserver(Observer* observer); - - private: - // base::PowerObserver implementation. - void OnPowerStateChange(bool on_battery_power) override; - - // Notifies |observers_| about battery requirement change. - void NotifyBatteryChange(SchedulingParams::BatteryRequirements); - - // Observers to monitor battery change in download service. - base::ObserverList<Observer> observers_; - - DISALLOW_COPY_AND_ASSIGN(BatteryListener); -}; - -} // namespace download - -#endif // COMPONENTS_DOWNLOAD_INTERNAL_SCHEDULER_BATTERY_LISTENER_H_ diff --git a/chromium/components/download/internal/scheduler/battery_listener_unittest.cc b/chromium/components/download/internal/scheduler/battery_listener_unittest.cc deleted file mode 100644 index 3ddacffbf10..00000000000 --- a/chromium/components/download/internal/scheduler/battery_listener_unittest.cc +++ /dev/null @@ -1,74 +0,0 @@ -// 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/scheduler/battery_listener.h" - -#include "base/memory/ptr_util.h" -#include "base/message_loop/message_loop.h" -#include "base/test/power_monitor_test_base.h" -#include "testing/gmock/include/gmock/gmock.h" -#include "testing/gtest/include/gtest/gtest.h" - -namespace download { -namespace { - -using BatteryRequirements = SchedulingParams::BatteryRequirements; - -class MockObserver : public BatteryListener::Observer { - public: - MOCK_METHOD1(OnBatteryChange, void(SchedulingParams::BatteryRequirements)); -}; - -class BatteryListenerTest : public testing::Test { - public: - BatteryListenerTest() {} - ~BatteryListenerTest() override = default; - - void CallBatteryChange(bool on_battery_power) { - DCHECK(listener_); - static_cast<base::PowerObserver*>(listener_.get()) - ->OnPowerStateChange(on_battery_power); - } - - void SetUp() override { - power_monitor_ = base::MakeUnique<base::PowerMonitor>( - base::MakeUnique<base::PowerMonitorTestSource>()); - - observer_ = base::MakeUnique<MockObserver>(); - listener_ = base::MakeUnique<BatteryListener>(); - listener_->AddObserver(observer_.get()); - } - - void TearDown() override { - listener_.reset(); - observer_.reset(); - } - - std::unique_ptr<BatteryListener> listener_; - std::unique_ptr<MockObserver> observer_; - - base::MessageLoop message_loop_; - std::unique_ptr<base::PowerMonitor> power_monitor_; - - private: - DISALLOW_COPY_AND_ASSIGN(BatteryListenerTest); -}; - -// Ensures observer methods are corrected called. -TEST_F(BatteryListenerTest, NotifyObservers) { - listener_->Start(); - EXPECT_CALL(*observer_.get(), - OnBatteryChange(BatteryRequirements::BATTERY_INSENSITIVE)) - .RetiresOnSaturation(); - CallBatteryChange(true); - - EXPECT_CALL(*observer_.get(), - OnBatteryChange(BatteryRequirements::BATTERY_SENSITIVE)) - .RetiresOnSaturation(); - CallBatteryChange(false); - listener_->Stop(); -}; - -} // namespace -} // namespace download diff --git a/chromium/components/download/internal/scheduler/device_status.cc b/chromium/components/download/internal/scheduler/device_status.cc new file mode 100644 index 00000000000..80c88c0053d --- /dev/null +++ b/chromium/components/download/internal/scheduler/device_status.cc @@ -0,0 +1,72 @@ +// 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/scheduler/device_status.h" + +namespace download { + +DeviceStatus::Result::Result() + : meets_battery_requirement(false), meets_network_requirement(false) {} + +bool DeviceStatus::Result::MeetsRequirements() const { + return meets_battery_requirement && meets_network_requirement; +} + +DeviceStatus::DeviceStatus() + : battery_status(BatteryStatus::NOT_CHARGING), + network_status(NetworkStatus::DISCONNECTED) {} + +DeviceStatus::DeviceStatus(BatteryStatus battery, NetworkStatus network) + : battery_status(battery), network_status(network) {} + +bool DeviceStatus::operator==(const DeviceStatus& rhs) const { + return network_status == rhs.network_status && + battery_status == rhs.battery_status; +} + +DeviceStatus::Result DeviceStatus::MeetsCondition( + const SchedulingParams& params) const { + DeviceStatus::Result result; + + switch (params.battery_requirements) { + case SchedulingParams::BatteryRequirements::BATTERY_INSENSITIVE: + result.meets_battery_requirement = true; + break; + case SchedulingParams::BatteryRequirements::BATTERY_SENSITIVE: + result.meets_battery_requirement = + battery_status == BatteryStatus::CHARGING; + break; + default: + NOTREACHED(); + } + switch (params.network_requirements) { + case SchedulingParams::NetworkRequirements::NONE: + result.meets_network_requirement = + network_status != NetworkStatus::DISCONNECTED; + break; + case SchedulingParams::NetworkRequirements::OPTIMISTIC: + case SchedulingParams::NetworkRequirements::UNMETERED: + result.meets_network_requirement = + network_status == NetworkStatus::UNMETERED; + break; + default: + NOTREACHED(); + } + return result; +} + +Criteria::Criteria() + : requires_battery_charging(true), requires_unmetered_network(true) {} + +Criteria::Criteria(bool requires_battery_charging, + bool requires_unmetered_network) + : requires_battery_charging(requires_battery_charging), + requires_unmetered_network(requires_unmetered_network) {} + +bool Criteria::operator==(const Criteria& other) const { + return requires_battery_charging == other.requires_battery_charging && + requires_unmetered_network == other.requires_unmetered_network; +} + +} // namespace download diff --git a/chromium/components/download/internal/scheduler/device_status.h b/chromium/components/download/internal/scheduler/device_status.h new file mode 100644 index 00000000000..720a1266f0f --- /dev/null +++ b/chromium/components/download/internal/scheduler/device_status.h @@ -0,0 +1,60 @@ +// 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. + +#ifndef COMPONENTS_DOWNLOAD_INTERNAL_SCHEDULER_DEVICE_STATUS_H_ +#define COMPONENTS_DOWNLOAD_INTERNAL_SCHEDULER_DEVICE_STATUS_H_ + +#include "components/download/public/download_params.h" + +namespace download { + +// Battery status used in download service. +enum class BatteryStatus { + CHARGING = 0, + NOT_CHARGING = 1, +}; + +// NetworkStatus should mostly one to one map to +// SchedulingParams::NetworkRequirements. Has coarser granularity than +// network connection type. +enum class NetworkStatus { + DISCONNECTED = 0, + UNMETERED = 1, // WIFI or Ethernet. + METERED = 2, // Mobile networks. +}; + +// Contains battery and network status. +struct DeviceStatus { + DeviceStatus(); + DeviceStatus(BatteryStatus battery, NetworkStatus network); + + struct Result { + Result(); + bool MeetsRequirements() const; + bool meets_battery_requirement; + bool meets_network_requirement; + }; + + BatteryStatus battery_status; + NetworkStatus network_status; + + bool operator==(const DeviceStatus& rhs) const; + + // Returns if the current device status meets all the conditions defined in + // the scheduling parameters. + Result MeetsCondition(const SchedulingParams& params) const; +}; + +// The criteria when the background download task should start. +struct Criteria { + Criteria(); + Criteria(bool requires_battery_charging, bool requires_unmetered_network); + bool operator==(const Criteria& other) const; + bool requires_battery_charging; + bool requires_unmetered_network; +}; + +} // namespace download + +#endif // COMPONENTS_DOWNLOAD_INTERNAL_SCHEDULER_DEVICE_STATUS_H_ diff --git a/chromium/components/download/internal/scheduler/device_status_listener.cc b/chromium/components/download/internal/scheduler/device_status_listener.cc new file mode 100644 index 00000000000..ba277b04489 --- /dev/null +++ b/chromium/components/download/internal/scheduler/device_status_listener.cc @@ -0,0 +1,122 @@ +// 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/scheduler/device_status_listener.h" + +#include "base/power_monitor/power_monitor.h" + +namespace download { + +namespace { + +// Converts |on_battery_power| to battery status. +BatteryStatus ToBatteryStatus(bool on_battery_power) { + return on_battery_power ? BatteryStatus::NOT_CHARGING + : BatteryStatus::CHARGING; +} + +// Converts a ConnectionType to NetworkStatus. +NetworkStatus ToNetworkStatus(net::NetworkChangeNotifier::ConnectionType type) { + switch (type) { + case net::NetworkChangeNotifier::CONNECTION_ETHERNET: + case net::NetworkChangeNotifier::CONNECTION_WIFI: + return NetworkStatus::UNMETERED; + case net::NetworkChangeNotifier::CONNECTION_2G: + case net::NetworkChangeNotifier::CONNECTION_3G: + case net::NetworkChangeNotifier::CONNECTION_4G: + return NetworkStatus::METERED; + case net::NetworkChangeNotifier::CONNECTION_UNKNOWN: + case net::NetworkChangeNotifier::CONNECTION_NONE: + case net::NetworkChangeNotifier::CONNECTION_BLUETOOTH: + return NetworkStatus::DISCONNECTED; + } + NOTREACHED(); + return NetworkStatus::DISCONNECTED; +} + +} // namespace + +DeviceStatusListener::DeviceStatusListener(const base::TimeDelta& delay) + : observer_(nullptr), listening_(false), delay_(delay) {} + +DeviceStatusListener::~DeviceStatusListener() { + Stop(); +} + +const DeviceStatus& DeviceStatusListener::CurrentDeviceStatus() const { + DCHECK(listening_) << "Call Start() before querying the status."; + return status_; +} + +void DeviceStatusListener::Start(DeviceStatusListener::Observer* observer) { + if (listening_) + return; + + DCHECK(observer); + observer_ = observer; + base::PowerMonitor* power_monitor = base::PowerMonitor::Get(); + DCHECK(power_monitor); + power_monitor->AddObserver(this); + + net::NetworkChangeNotifier::AddConnectionTypeObserver(this); + + status_.battery_status = + ToBatteryStatus(base::PowerMonitor::Get()->IsOnBatteryPower()); + status_.network_status = + ToNetworkStatus(net::NetworkChangeNotifier::GetConnectionType()); + listening_ = true; +} + +void DeviceStatusListener::Stop() { + if (!listening_) + return; + + base::PowerMonitor::Get()->RemoveObserver(this); + net::NetworkChangeNotifier::RemoveConnectionTypeObserver(this); + + status_ = DeviceStatus(); + listening_ = false; + observer_ = nullptr; +} + +void DeviceStatusListener::OnConnectionTypeChanged( + net::NetworkChangeNotifier::ConnectionType type) { + NetworkStatus new_network_status = ToNetworkStatus(type); + if (status_.network_status == new_network_status) + return; + + bool change_to_online = + (status_.network_status == NetworkStatus::DISCONNECTED) && + (new_network_status != NetworkStatus::DISCONNECTED); + + // It's unreliable to send requests immediately after the network becomes + // online. Notify network change to the observer after a delay. + if (change_to_online) { + timer_.Start( + FROM_HERE, delay_, + base::Bind(&DeviceStatusListener::NotifyNetworkChangeAfterDelay, + base::Unretained(this), new_network_status)); + } else { + status_.network_status = new_network_status; + timer_.Stop(); + NotifyStatusChange(); + } +} + +void DeviceStatusListener::OnPowerStateChange(bool on_battery_power) { + status_.battery_status = ToBatteryStatus(on_battery_power); + NotifyStatusChange(); +} + +void DeviceStatusListener::NotifyStatusChange() { + observer_->OnDeviceStatusChanged(status_); +} + +void DeviceStatusListener::NotifyNetworkChangeAfterDelay( + NetworkStatus network_status) { + status_.network_status = network_status; + NotifyStatusChange(); +} + +} // namespace download diff --git a/chromium/components/download/internal/scheduler/device_status_listener.h b/chromium/components/download/internal/scheduler/device_status_listener.h new file mode 100644 index 00000000000..a14a28c6f84 --- /dev/null +++ b/chromium/components/download/internal/scheduler/device_status_listener.h @@ -0,0 +1,72 @@ +// 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. + +#ifndef COMPONENTS_DOWNLOAD_INTERNAL_SCHEDULER_DEVICE_STATUS_LISTENER_H_ +#define COMPONENTS_DOWNLOAD_INTERNAL_SCHEDULER_DEVICE_STATUS_LISTENER_H_ + +#include "base/power_monitor/power_observer.h" +#include "base/timer/timer.h" +#include "components/download/internal/scheduler/device_status.h" +#include "net/base/network_change_notifier.h" + +namespace download { + +// Listens to network and battery status change and notifies the observer. +class DeviceStatusListener + : public net::NetworkChangeNotifier::ConnectionTypeObserver, + public base::PowerObserver { + public: + class Observer { + public: + // Called when device status is changed. + virtual void OnDeviceStatusChanged(const DeviceStatus& device_status) = 0; + }; + + DeviceStatusListener(const base::TimeDelta& delay); + ~DeviceStatusListener() override; + + // Returns the current device status for download scheduling. + const DeviceStatus& CurrentDeviceStatus() const; + + // Starts/stops to listen network and battery change events, virtual for + // testing. + virtual void Start(DeviceStatusListener::Observer* observer); + virtual void Stop(); + + protected: + // The current device status. + DeviceStatus status_; + + // The observer that listens to device status change events. + Observer* observer_; + + // If we are actively listening to network and battery change events. + bool listening_; + + private: + // net::NetworkChangeNotifier::ConnectionTypeObserver implementation. + void OnConnectionTypeChanged( + net::NetworkChangeNotifier::ConnectionType type) override; + + // base::PowerObserver implementation. + void OnPowerStateChange(bool on_battery_power) override; + + // Notifies the observer about device status change. + void NotifyStatusChange(); + + // Called after a delay to notify the observer. See |delay_|. + void NotifyNetworkChangeAfterDelay(NetworkStatus network_status); + + // Used to notify the observer after a delay when network becomes connected. + base::OneShotTimer timer_; + + // The delay used by |timer_|. + base::TimeDelta delay_; + + DISALLOW_COPY_AND_ASSIGN(DeviceStatusListener); +}; + +} // namespace download + +#endif // COMPONENTS_DOWNLOAD_INTERNAL_SCHEDULER_DEVICE_STATUS_LISTENER_H_ diff --git a/chromium/components/download/internal/scheduler/device_status_listener_unittest.cc b/chromium/components/download/internal/scheduler/device_status_listener_unittest.cc new file mode 100644 index 00000000000..45fea61651c --- /dev/null +++ b/chromium/components/download/internal/scheduler/device_status_listener_unittest.cc @@ -0,0 +1,146 @@ +// 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/scheduler/device_status_listener.h" + +#include <memory> + +#include "base/memory/ptr_util.h" +#include "base/message_loop/message_loop.h" +#include "base/run_loop.h" +#include "base/test/power_monitor_test_base.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using testing::InSequence; +using ConnectionTypeObserver = + net::NetworkChangeNotifier::ConnectionTypeObserver; +using ConnectionType = net::NetworkChangeNotifier::ConnectionType; + +namespace download { +namespace { + +// NetworkChangeNotifier that can change network type in tests. +class TestNetworkChangeNotifier : public net::NetworkChangeNotifier { + public: + TestNetworkChangeNotifier() + : net::NetworkChangeNotifier(), + conn_type_(ConnectionType::CONNECTION_UNKNOWN) {} + + // net::NetworkChangeNotifier implementation. + ConnectionType GetCurrentConnectionType() const override { + return conn_type_; + } + + // Changes the network type. + void ChangeNetworkType(ConnectionType type) { + conn_type_ = type; + net::NetworkChangeNotifier::NotifyObserversOfConnectionTypeChangeForTests( + type); + } + + private: + ConnectionType conn_type_; + + DISALLOW_COPY_AND_ASSIGN(TestNetworkChangeNotifier); +}; + +class MockObserver : public DeviceStatusListener::Observer { + public: + MOCK_METHOD1(OnDeviceStatusChanged, void(const DeviceStatus&)); +}; + +class DeviceStatusListenerTest : public testing::Test { + public: + void SetUp() override { + power_monitor_ = base::MakeUnique<base::PowerMonitor>( + base::MakeUnique<base::PowerMonitorTestSource>()); + + listener_ = + base::MakeUnique<DeviceStatusListener>(base::TimeDelta::FromSeconds(0)); + } + + void TearDown() override { listener_.reset(); } + + // Simulates a network change call. + void ChangeNetworkType(ConnectionType type) { + test_network_notifier_.ChangeNetworkType(type); + } + + // Simulates a battery change call. + void SimulateBatteryChange(bool on_battery_power) { + static_cast<base::PowerObserver*>(listener_.get()) + ->OnPowerStateChange(on_battery_power); + } + + protected: + std::unique_ptr<DeviceStatusListener> listener_; + MockObserver mock_observer_; + + // Needed for network change notifier and power monitor. + base::MessageLoop message_loop_; + TestNetworkChangeNotifier test_network_notifier_; + std::unique_ptr<base::PowerMonitor> power_monitor_; +}; + +// Ensures the observer is notified when network condition changes. +TEST_F(DeviceStatusListenerTest, NotifyObserverNetworkChange) { + listener_->Start(&mock_observer_); + + // Initial states check. + DeviceStatus status = listener_->CurrentDeviceStatus(); + EXPECT_EQ(NetworkStatus::DISCONNECTED, status.network_status); + + // Network switch between mobile networks, the observer should be notified + // only once. + status.network_status = NetworkStatus::METERED; + EXPECT_CALL(mock_observer_, OnDeviceStatusChanged(status)) + .Times(1) + .RetiresOnSaturation(); + + ChangeNetworkType(ConnectionType::CONNECTION_4G); + ChangeNetworkType(ConnectionType::CONNECTION_3G); + ChangeNetworkType(ConnectionType::CONNECTION_2G); + + // Verifies the online signal is sent in a post task after a delay. + EXPECT_EQ(NetworkStatus::DISCONNECTED, + listener_->CurrentDeviceStatus().network_status); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(NetworkStatus::METERED, + listener_->CurrentDeviceStatus().network_status); + + // Network is switched between wifi and ethernet, the observer should be + // notified only once. + status.network_status = NetworkStatus::UNMETERED; + EXPECT_CALL(mock_observer_, OnDeviceStatusChanged(status)) + .Times(1) + .RetiresOnSaturation(); + + ChangeNetworkType(ConnectionType::CONNECTION_WIFI); + ChangeNetworkType(ConnectionType::CONNECTION_ETHERNET); + + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(NetworkStatus::UNMETERED, + listener_->CurrentDeviceStatus().network_status); +} + +// Ensures the observer is notified when battery condition changes. +TEST_F(DeviceStatusListenerTest, NotifyObserverBatteryChange) { + InSequence s; + listener_->Start(&mock_observer_); + DeviceStatus status = listener_->CurrentDeviceStatus(); + status.battery_status = BatteryStatus::NOT_CHARGING; + EXPECT_CALL(mock_observer_, OnDeviceStatusChanged(status)) + .RetiresOnSaturation(); + SimulateBatteryChange(true); + + status.battery_status = BatteryStatus::CHARGING; + EXPECT_CALL(mock_observer_, OnDeviceStatusChanged(status)) + .RetiresOnSaturation(); + SimulateBatteryChange(false); + listener_->Stop(); +}; + +} // namespace +} // namespace download diff --git a/chromium/components/download/internal/scheduler/network_listener.cc b/chromium/components/download/internal/scheduler/network_listener.cc deleted file mode 100644 index d09d52773ee..00000000000 --- a/chromium/components/download/internal/scheduler/network_listener.cc +++ /dev/null @@ -1,84 +0,0 @@ -// 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/scheduler/network_listener.h" - -namespace download { - -namespace { - -// Converts a ConnectionType to NetworkListener::NetworkStatus. -NetworkListener::NetworkStatus ToNetworkStatus( - net::NetworkChangeNotifier::ConnectionType type) { - switch (type) { - case net::NetworkChangeNotifier::CONNECTION_ETHERNET: - case net::NetworkChangeNotifier::CONNECTION_WIFI: - return NetworkListener::NetworkStatus::UNMETERED; - case net::NetworkChangeNotifier::CONNECTION_2G: - case net::NetworkChangeNotifier::CONNECTION_3G: - case net::NetworkChangeNotifier::CONNECTION_4G: - return NetworkListener::NetworkStatus::METERED; - case net::NetworkChangeNotifier::CONNECTION_UNKNOWN: - case net::NetworkChangeNotifier::CONNECTION_NONE: - case net::NetworkChangeNotifier::CONNECTION_BLUETOOTH: - return NetworkListener::NetworkStatus::DISCONNECTED; - } - NOTREACHED(); - return NetworkListener::NetworkStatus::DISCONNECTED; -} - -} // namespace - -NetworkListener::NetworkListener() - : status_(NetworkStatus::DISCONNECTED), listening_(false) {} - -NetworkListener::~NetworkListener() { - Stop(); -} - -NetworkListener::NetworkStatus NetworkListener::CurrentNetworkStatus() const { - return ToNetworkStatus(net::NetworkChangeNotifier::GetConnectionType()); -} - -void NetworkListener::Start() { - if (listening_) - return; - - net::NetworkChangeNotifier::AddConnectionTypeObserver(this); - status_ = ToNetworkStatus(net::NetworkChangeNotifier::GetConnectionType()); - listening_ = true; -} - -void NetworkListener::Stop() { - if (!listening_) - return; - - net::NetworkChangeNotifier::RemoveConnectionTypeObserver(this); - status_ = ToNetworkStatus(net::NetworkChangeNotifier::CONNECTION_UNKNOWN); - listening_ = false; -} - -void NetworkListener::AddObserver(Observer* observer) { - observers_.AddObserver(observer); -} - -void NetworkListener::RemoveObserver(Observer* observer) { - observers_.RemoveObserver(observer); -} - -void NetworkListener::OnConnectionTypeChanged( - net::NetworkChangeNotifier::ConnectionType type) { - NetworkStatus new_status = ToNetworkStatus(type); - if (status_ != new_status) { - status_ = new_status; - NotifyNetworkChange(status_); - } -} - -void NetworkListener::NotifyNetworkChange(NetworkStatus network_status) { - for (auto& observer : observers_) - observer.OnNetworkChange(network_status); -} - -} // namespace download diff --git a/chromium/components/download/internal/scheduler/network_listener.h b/chromium/components/download/internal/scheduler/network_listener.h deleted file mode 100644 index 6b05ffb16b0..00000000000 --- a/chromium/components/download/internal/scheduler/network_listener.h +++ /dev/null @@ -1,66 +0,0 @@ -// 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. - -#ifndef COMPONENTS_DOWNLOAD_INTERNAL_SCHEDULER_NETWORK_LISTENER_H_ -#define COMPONENTS_DOWNLOAD_INTERNAL_SCHEDULER_NETWORK_LISTENER_H_ - -#include "net/base/network_change_notifier.h" - -namespace download { - -// Listens to network status change and notifies observers in download service. -class NetworkListener - : public net::NetworkChangeNotifier::ConnectionTypeObserver { - public: - // NetworkStatus should mostly one to one map to - // SchedulingParams::NetworkRequirements. Has coarser granularity than - // network connection type. - enum class NetworkStatus { - DISCONNECTED = 0, - UNMETERED = 1, // WIFI or Ethernet. - METERED = 2, // Mobile networks. - }; - - class Observer { - public: - // Called when network status is changed. - virtual void OnNetworkChange(NetworkStatus network_status) = 0; - }; - - NetworkListener(); - ~NetworkListener() override; - - // Returns the current network status for download scheduling. - NetworkStatus CurrentNetworkStatus() const; - - // Starts/stops to listen network change events. - void Start(); - void Stop(); - - // Adds/Removes observers. - void AddObserver(Observer* observer); - void RemoveObserver(Observer* observer); - - private: - // net::NetworkChangeNotifier implementation. - void OnConnectionTypeChanged( - net::NetworkChangeNotifier::ConnectionType type) override; - - // Notifies |observers_| about network status change. See |NetworkStatus|. - void NotifyNetworkChange(NetworkStatus network_status); - - // The current network status. - NetworkStatus status_; - - // If we are actively listening to network change events. - bool listening_; - - base::ObserverList<Observer> observers_; - - DISALLOW_COPY_AND_ASSIGN(NetworkListener); -}; - -} // namespace download - -#endif // COMPONENTS_DOWNLOAD_INTERNAL_SCHEDULER_NETWORK_LISTENER_H_ diff --git a/chromium/components/download/internal/scheduler/network_listener_unittest.cc b/chromium/components/download/internal/scheduler/network_listener_unittest.cc deleted file mode 100644 index 4b8165dcef1..00000000000 --- a/chromium/components/download/internal/scheduler/network_listener_unittest.cc +++ /dev/null @@ -1,100 +0,0 @@ -// 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/scheduler/network_listener.h" - -#include "base/memory/ptr_util.h" -#include "base/message_loop/message_loop.h" -#include "base/run_loop.h" -#include "testing/gmock/include/gmock/gmock.h" -#include "testing/gtest/include/gtest/gtest.h" - -using ConnectionTypeObserver = - net::NetworkChangeNotifier::ConnectionTypeObserver; -using ConnectionType = net::NetworkChangeNotifier::ConnectionType; - -namespace download { -namespace { - -// NetworkChangeNotifier that can change network type in tests. -class TestNetworkChangeNotifier : public net::NetworkChangeNotifier { - public: - TestNetworkChangeNotifier() - : net::NetworkChangeNotifier(), - conn_type_(ConnectionType::CONNECTION_UNKNOWN) {} - - // net::NetworkChangeNotifier implementation. - ConnectionType GetCurrentConnectionType() const override { - return conn_type_; - } - - // Change the network type. - void ChangeNetworkType(ConnectionType type) { - conn_type_ = type; - net::NetworkChangeNotifier::NotifyObserversOfConnectionTypeChangeForTests( - type); - base::RunLoop().RunUntilIdle(); - } - - private: - ConnectionType conn_type_; - - DISALLOW_COPY_AND_ASSIGN(TestNetworkChangeNotifier); -}; - -class MockObserver : public NetworkListener::Observer { - public: - MOCK_METHOD1(OnNetworkChange, void(NetworkListener::NetworkStatus)); -}; - -class NetworkListenerTest : public testing::Test { - public: - // Simulates a network change call. - void ChangeNetworkType(ConnectionType type) { - test_network_notifier_.ChangeNetworkType(type); - } - - protected: - NetworkListener network_listener_; - MockObserver mock_observer_; - - // Needed for network change notifier. - base::MessageLoop message_loop_; - TestNetworkChangeNotifier test_network_notifier_; -}; - -TEST_F(NetworkListenerTest, NotifyObserverNetworkChange) { - network_listener_.Start(); - network_listener_.AddObserver(&mock_observer_); - - // Initial states check. - EXPECT_EQ(NetworkListener::NetworkStatus::DISCONNECTED, - network_listener_.CurrentNetworkStatus()); - - // Network switch between mobile networks, the observer should be notified - // only once. - EXPECT_CALL(mock_observer_, - OnNetworkChange(NetworkListener::NetworkStatus::METERED)) - .Times(1) - .RetiresOnSaturation(); - - ChangeNetworkType(ConnectionType::CONNECTION_4G); - ChangeNetworkType(ConnectionType::CONNECTION_3G); - ChangeNetworkType(ConnectionType::CONNECTION_2G); - EXPECT_EQ(NetworkListener::NetworkStatus::METERED, - network_listener_.CurrentNetworkStatus()); - - // Network is switched between wifi and ethernet, the observer should be - // notified only once. - EXPECT_CALL(mock_observer_, - OnNetworkChange(NetworkListener::NetworkStatus::UNMETERED)) - .Times(1) - .RetiresOnSaturation(); - - ChangeNetworkType(ConnectionType::CONNECTION_WIFI); - ChangeNetworkType(ConnectionType::CONNECTION_ETHERNET); -} - -} // namespace -} // namespace download diff --git a/chromium/components/download/internal/scheduler/scheduler.h b/chromium/components/download/internal/scheduler/scheduler.h new file mode 100644 index 00000000000..58d835a3ab3 --- /dev/null +++ b/chromium/components/download/internal/scheduler/scheduler.h @@ -0,0 +1,36 @@ +// 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. + +#ifndef COMPONENTS_DOWNLOAD_INTERNAL_SCHEDULER_SCHEDULER_H_ +#define COMPONENTS_DOWNLOAD_INTERNAL_SCHEDULER_SCHEDULER_H_ + +#include "components/download/internal/model.h" +#include "components/download/internal/scheduler/device_status.h" +#include "components/download/public/download_params.h" + +namespace download { + +struct DeviceStatus; + +// The interface that talks to download service to schedule platform background +// download tasks. +class Scheduler { + public: + // Reschedule another background platform task based on the scheduling + // parameters of |entries|. Should only pass in entries in active or available + // state. + virtual void Reschedule(const Model::EntryList& entries) = 0; + + // Returns the next download that should be processed based on scheduling + // parameters, may return nullptr if no download meets the criteria. + // The sequence of polling on entries with exactly same states is undefined. + virtual Entry* Next(const Model::EntryList& entries, + const DeviceStatus& device_status) = 0; + + virtual ~Scheduler() {} +}; + +} // namespace download + +#endif // COMPONENTS_DOWNLOAD_CORE_SCHEDULER_H_ diff --git a/chromium/components/download/internal/scheduler/scheduler_impl.cc b/chromium/components/download/internal/scheduler/scheduler_impl.cc new file mode 100644 index 00000000000..8ee8f7381f6 --- /dev/null +++ b/chromium/components/download/internal/scheduler/scheduler_impl.cc @@ -0,0 +1,131 @@ +// 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/scheduler/scheduler_impl.h" + +#include "components/download/internal/client_set.h" +#include "components/download/internal/config.h" +#include "components/download/internal/entry_utils.h" +#include "components/download/internal/scheduler/device_status.h" +#include "components/download/public/download_params.h" +#include "components/download/public/task_scheduler.h" + +namespace download { + +namespace { + +// Returns a vector of elements contained in the |set|. +template <typename T> +std::vector<T> ToList(const std::set<T>& set) { + std::vector<T> list; + for (const auto& element : set) { + list.push_back(element); + } + return list; +} + +} // namespace + +SchedulerImpl::SchedulerImpl(TaskScheduler* task_scheduler, + Configuration* config, + const ClientSet* clients) + : SchedulerImpl(task_scheduler, + config, + ToList<DownloadClient>(clients->GetRegisteredClients())) {} + +SchedulerImpl::SchedulerImpl(TaskScheduler* task_scheduler, + Configuration* config, + const std::vector<DownloadClient>& clients) + : task_scheduler_(task_scheduler), + config_(config), + download_clients_(clients), + current_client_index_(0) { + DCHECK(task_scheduler_); +} + +SchedulerImpl::~SchedulerImpl() = default; + +void SchedulerImpl::Reschedule(const Model::EntryList& entries) { + if (entries.empty()) { + task_scheduler_->CancelTask(DownloadTaskType::DOWNLOAD_TASK); + return; + } + + // TODO(xingliu): Support NetworkRequirements::OPTIMISTIC. + task_scheduler_->CancelTask(DownloadTaskType::DOWNLOAD_TASK); + + Criteria criteria = util::GetSchedulingCriteria(entries); + task_scheduler_->ScheduleTask( + DownloadTaskType::DOWNLOAD_TASK, criteria.requires_unmetered_network, + criteria.requires_battery_charging, + base::saturated_cast<long>(config_->window_start_time.InSeconds()), + base::saturated_cast<long>(config_->window_end_time.InSeconds())); +} + +Entry* SchedulerImpl::Next(const Model::EntryList& entries, + const DeviceStatus& device_status) { + std::map<DownloadClient, Entry*> candidates = + FindCandidates(entries, device_status); + + Entry* entry = nullptr; + size_t index = current_client_index_; + + // Finds the next entry to download. + for (size_t i = 0; i < download_clients_.size(); ++i) { + DownloadClient client = + download_clients_[(index + i) % download_clients_.size()]; + Entry* candidate = candidates[client]; + + // Some clients may have no entries, continue to check other clients. + if (!candidate) + continue; + + bool ui_priority = + candidate->scheduling_params.priority == SchedulingParams::Priority::UI; + + // Records the first available candidate. Keep iterating to see if there + // are UI priority entries for other clients. + if (!entry || ui_priority) { + entry = candidate; + + // Load balancing between clients. + current_client_index_ = (index + i + 1) % download_clients_.size(); + + // UI priority entry will be processed immediately. + if (ui_priority) + break; + } + } + return entry; +} + +std::map<DownloadClient, Entry*> SchedulerImpl::FindCandidates( + const Model::EntryList& entries, + const DeviceStatus& device_status) { + std::map<DownloadClient, Entry*> candidates; + + if (entries.empty()) + return candidates; + + for (auto* const entry : entries) { + DCHECK(entry); + const SchedulingParams& current_params = entry->scheduling_params; + + // Every download needs to pass the state and device status check. + if (entry->state != Entry::State::AVAILABLE || + !device_status.MeetsCondition(current_params).MeetsRequirements()) { + continue; + } + + // Find the most appropriate download based on priority and cancel time. + Entry* candidate = candidates[entry->client]; + if (!candidate || util::EntryBetterThan(*entry, *candidate)) { + candidates[entry->client] = entry; + } + } + + return candidates; +} + +} // namespace download diff --git a/chromium/components/download/internal/scheduler/scheduler_impl.h b/chromium/components/download/internal/scheduler/scheduler_impl.h new file mode 100644 index 00000000000..1284d38fe0b --- /dev/null +++ b/chromium/components/download/internal/scheduler/scheduler_impl.h @@ -0,0 +1,72 @@ +// 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. + +#ifndef COMPONENTS_DOWNLOAD_INTERNAL_SCHEDULER_SCHEDULER_IMPL_H_ +#define COMPONENTS_DOWNLOAD_INTERNAL_SCHEDULER_SCHEDULER_IMPL_H_ + +#include "components/download/internal/scheduler/scheduler.h" + +#include <map> +#include <vector> + +#include "base/macros.h" +#include "components/download/internal/entry.h" + +namespace download { + +class ClientSet; +class TaskScheduler; +struct Configuration; + +// Scheduler implementation that +// 1. Creates platform background task based on the states of download entries. +// 2. Polls the next entry to be processed by the service mainly according to +// scheduling parameters and current device status. +// +// Provides load balancing between download clients using the service. +class SchedulerImpl : public Scheduler { + public: + SchedulerImpl(TaskScheduler* task_scheduler, + Configuration* config, + const ClientSet* clients); + SchedulerImpl(TaskScheduler* task_scheduler, + Configuration* config, + const std::vector<DownloadClient>& clients); + ~SchedulerImpl() override; + + // Scheduler implementation. + void Reschedule(const Model::EntryList& entries) override; + Entry* Next(const Model::EntryList& entries, + const DeviceStatus& device_status) override; + + private: + // Finds a candidate for each download client to be processed next by the + // service. + // The candidates are selected based on scheduling parameters and current + // device status. + std::map<DownloadClient, Entry*> FindCandidates( + const Model::EntryList& entries, + const DeviceStatus& device_status); + + // Used to create platform dependent background tasks. + TaskScheduler* task_scheduler_; + + // Download service configuration. + Configuration* config_; + + // List of all download client id, used in round robin load balancing. + // Downloads will be delivered to clients with incremental order based on + // the index of this list. + const std::vector<DownloadClient> download_clients_; + + // The index of the current client. + // See |download_clients_|. + size_t current_client_index_; + + DISALLOW_COPY_AND_ASSIGN(SchedulerImpl); +}; + +} // namespace download + +#endif // COMPONENTS_DOWNLOAD_INTERNAL_SCHEDULER_SCHEDULER_IMPL_H_ diff --git a/chromium/components/download/internal/scheduler/scheduler_impl_unittest.cc b/chromium/components/download/internal/scheduler/scheduler_impl_unittest.cc new file mode 100644 index 00000000000..482d8141805 --- /dev/null +++ b/chromium/components/download/internal/scheduler/scheduler_impl_unittest.cc @@ -0,0 +1,429 @@ +// 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/scheduler/scheduler_impl.h" + +#include <memory> + +#include "base/memory/ptr_util.h" +#include "base/strings/string_number_conversions.h" +#include "components/download/internal/config.h" +#include "components/download/internal/entry.h" +#include "components/download/internal/scheduler/device_status.h" +#include "components/download/public/task_scheduler.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using testing::_; +using testing::InSequence; + +namespace download { +namespace { + +class MockTaskScheduler : public TaskScheduler { + public: + MockTaskScheduler() = default; + ~MockTaskScheduler() override = default; + + MOCK_METHOD5(ScheduleTask, void(DownloadTaskType, bool, bool, long, long)); + MOCK_METHOD1(CancelTask, void(DownloadTaskType)); +}; + +class DownloadSchedulerImplTest : public testing::Test { + public: + DownloadSchedulerImplTest() {} + ~DownloadSchedulerImplTest() override = default; + + void TearDown() override { DestroyScheduler(); } + + void BuildScheduler(const std::vector<DownloadClient> clients) { + scheduler_ = + base::MakeUnique<SchedulerImpl>(&task_scheduler_, &config_, clients); + } + void DestroyScheduler() { scheduler_.reset(); } + + // Helper function to create a list of entries for the scheduler to query the + // next entry. + void BuildDataEntries(size_t size) { + entries_ = std::vector<Entry>(size, Entry()); + for (size_t i = 0; i < size; ++i) { + entries_[i].guid = base::IntToString(i); + entries_[i].scheduling_params.battery_requirements = + SchedulingParams::BatteryRequirements::BATTERY_SENSITIVE; + entries_[i].scheduling_params.network_requirements = + SchedulingParams::NetworkRequirements::UNMETERED; + entries_[i].state = Entry::State::AVAILABLE; + } + } + + // Returns list of entry pointers to feed to the scheduler. + Model::EntryList entries() { + Model::EntryList entry_list; + for (auto& entry : entries_) { + entry_list.emplace_back(&entry); + } + return entry_list; + } + + // Simulates the entry has been processed by the download service and the + // state has changed. + void MakeEntryActive(Entry* entry) { + if (entry) + entry->state = Entry::State::ACTIVE; + } + + // Reverts the states of entry so that the scheduler can poll it again. + void MakeEntryAvailable(Entry* entry) { + entry->state = Entry::State::AVAILABLE; + } + + // Helper function to build a device status. + DeviceStatus BuildDeviceStatus(BatteryStatus battery, NetworkStatus network) { + DeviceStatus device_status; + device_status.battery_status = battery; + device_status.network_status = network; + return device_status; + } + + protected: + std::unique_ptr<SchedulerImpl> scheduler_; + MockTaskScheduler task_scheduler_; + Configuration config_; + + // Entries owned by the test fixture. + std::vector<Entry> entries_; + + private: + DISALLOW_COPY_AND_ASSIGN(DownloadSchedulerImplTest); +}; + +// Ensures normal polling logic is correct. +TEST_F(DownloadSchedulerImplTest, BasicPolling) { + BuildScheduler(std::vector<DownloadClient>{DownloadClient::TEST_2, + DownloadClient::TEST}); + + // Client TEST: entry 0. + // Client TEST_2: entry 1. + // Poll sequence: 1 -> 0. + BuildDataEntries(2); + entries_[0].client = DownloadClient::TEST; + entries_[1].client = DownloadClient::TEST_2; + + // First download belongs to first client. + Entry* next = scheduler_->Next( + entries(), + BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED)); + EXPECT_EQ(next, &entries_[1]); + MakeEntryActive(next); + + // If the first one is processed, the next should be the other entry. + next = scheduler_->Next( + entries(), + BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED)); + EXPECT_EQ(next, &entries_[0]); + MakeEntryActive(next); +} + +// Tests the load balancing and polling downloads based on cancel time. +TEST_F(DownloadSchedulerImplTest, BasicLoadBalancing) { + BuildScheduler(std::vector<DownloadClient>{ + DownloadClient::TEST, DownloadClient::TEST_2, DownloadClient::TEST_3}); + + // Client TEST: entry 0, entry 1 (earlier cancel time). + // Client TEST_2: entry 2. + // Client TEST_3: No entries. + // Poll sequence: 1 -> 2 -> 0. + BuildDataEntries(3); + entries_[0].client = DownloadClient::TEST; + entries_[0].scheduling_params.cancel_time = base::Time::FromInternalValue(20); + entries_[1].client = DownloadClient::TEST; + entries_[1].scheduling_params.cancel_time = base::Time::FromInternalValue(10); + entries_[2].client = DownloadClient::TEST_2; + entries_[2].scheduling_params.cancel_time = base::Time::FromInternalValue(30); + + // There are 2 downloads for client 0, the one with earlier create time will + // be the next download. + Entry* next = scheduler_->Next( + entries(), + BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED)); + EXPECT_EQ(&entries_[1], next); + MakeEntryActive(next); + + // The second download should belongs to client 1, because of the round robin + // load balancing. + next = scheduler_->Next( + entries(), + BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED)); + EXPECT_EQ(&entries_[2], next); + MakeEntryActive(next); + + // Only one entry left, which will be the next. + next = scheduler_->Next( + entries(), + BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED)); + EXPECT_EQ(&entries_[0], next); + MakeEntryActive(next); + + // Keep polling twice, since no available downloads, both will return nullptr. + next = scheduler_->Next( + entries(), + BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED)); + EXPECT_EQ(nullptr, next); + MakeEntryActive(next); + + next = scheduler_->Next( + entries(), + BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED)); + EXPECT_EQ(nullptr, next); + MakeEntryActive(next); +} + +// Ensures downloads are polled based on scheduling parameters and device +// status. +TEST_F(DownloadSchedulerImplTest, SchedulingParams) { + BuildScheduler(std::vector<DownloadClient>{DownloadClient::TEST}); + BuildDataEntries(1); + entries_[0].client = DownloadClient::TEST; + entries_[0].scheduling_params.battery_requirements = + SchedulingParams::BatteryRequirements::BATTERY_SENSITIVE; + entries_[0].scheduling_params.network_requirements = + SchedulingParams::NetworkRequirements::UNMETERED; + + Entry* next = nullptr; + + // Tests network scheduling parameter. + // No downloads can be polled when network disconnected. + next = scheduler_->Next( + entries(), + BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::DISCONNECTED)); + EXPECT_EQ(nullptr, next); + + // If the network is metered, and scheduling parameter requires unmetered + // network, the download should not be polled. + next = scheduler_->Next(entries(), BuildDeviceStatus(BatteryStatus::CHARGING, + NetworkStatus::METERED)); + EXPECT_EQ(nullptr, next); + + // If the network requirement is none, the download can happen under metered + // network. However, download won't happen when network is disconnected. + entries_[0].scheduling_params.network_requirements = + SchedulingParams::NetworkRequirements::NONE; + next = scheduler_->Next(entries(), BuildDeviceStatus(BatteryStatus::CHARGING, + NetworkStatus::METERED)); + EXPECT_EQ(&entries_[0], next); + next = scheduler_->Next( + entries(), + BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::DISCONNECTED)); + EXPECT_EQ(nullptr, next); + MakeEntryActive(next); + + // Tests battery sensitive scheduling parameter. + next = scheduler_->Next( + entries(), + BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED)); + EXPECT_EQ(&entries_[0], next); + next = scheduler_->Next( + entries(), + BuildDeviceStatus(BatteryStatus::NOT_CHARGING, NetworkStatus::UNMETERED)); + EXPECT_EQ(nullptr, next); + MakeEntryActive(next); + + entries_[0].scheduling_params.battery_requirements = + SchedulingParams::BatteryRequirements::BATTERY_INSENSITIVE; + next = scheduler_->Next( + entries(), + BuildDeviceStatus(BatteryStatus::NOT_CHARGING, NetworkStatus::UNMETERED)); + EXPECT_EQ(&entries_[0], next); + next = scheduler_->Next( + entries(), + BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED)); + EXPECT_EQ(&entries_[0], next); + MakeEntryActive(next); +} + +// Ensures higher priority will be scheduled first. +TEST_F(DownloadSchedulerImplTest, Priority) { + BuildScheduler(std::vector<DownloadClient>{DownloadClient::TEST}); + + // The second entry has higher priority but is created later than the first + // entry. This ensures priority is checked before the create time. + BuildDataEntries(2); + entries_[0].client = DownloadClient::TEST; + entries_[0].scheduling_params.priority = SchedulingParams::Priority::LOW; + entries_[0].scheduling_params.cancel_time = base::Time::FromInternalValue(20); + entries_[1].client = DownloadClient::TEST; + entries_[1].scheduling_params.priority = SchedulingParams::Priority::HIGH; + entries_[1].scheduling_params.cancel_time = base::Time::FromInternalValue(40); + + // Download with higher priority should be polled first, even if there is + // another download created earlier. + Entry* next = scheduler_->Next( + entries(), + BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED)); + EXPECT_EQ(&entries_[1], next); + + // Download with non UI priority should be subject to network and battery + // scheduling parameters. The higher priority one will be ignored because of + // mismatching battery condition. + entries_[1].scheduling_params.battery_requirements = + SchedulingParams::BatteryRequirements::BATTERY_SENSITIVE; + entries_[0].scheduling_params.battery_requirements = + SchedulingParams::BatteryRequirements::BATTERY_INSENSITIVE; + + next = scheduler_->Next( + entries(), + BuildDeviceStatus(BatteryStatus::NOT_CHARGING, NetworkStatus::UNMETERED)); + EXPECT_EQ(&entries_[0], next); + MakeEntryActive(next); +} + +// Ensures UI priority entries are subject to device status check. +TEST_F(DownloadSchedulerImplTest, UIPrioritySubjectToDeviceStatus) { + BuildScheduler(std::vector<DownloadClient>{DownloadClient::TEST, + DownloadClient::TEST_2}); + + // Client TEST: entry 0. + // Client TEST_2: entry 1 (UI priority, cancel later). + BuildDataEntries(2); + entries_[0].client = DownloadClient::TEST; + entries_[0].scheduling_params.priority = SchedulingParams::Priority::LOW; + entries_[1].client = DownloadClient::TEST_2; + entries_[1].scheduling_params.priority = SchedulingParams::Priority::UI; + + // UI priority is also subject to device status validation. + Entry* next = scheduler_->Next( + entries(), + BuildDeviceStatus(BatteryStatus::NOT_CHARGING, NetworkStatus::METERED)); + EXPECT_EQ(nullptr, next); + MakeEntryActive(next); +} + +// UI priority entries will be processed first even if they doesn't belong to +// the current client in load balancing. +TEST_F(DownloadSchedulerImplTest, UIPriorityLoadBalancing) { + BuildScheduler(std::vector<DownloadClient>{DownloadClient::TEST, + DownloadClient::TEST_2}); + + // Client TEST: entry 0(Low priority). + // Client TEST_2: entry 1(UI priority). + BuildDataEntries(2); + entries_[0].client = DownloadClient::TEST; + entries_[0].scheduling_params.priority = SchedulingParams::Priority::LOW; + entries_[1].client = DownloadClient::TEST_2; + entries_[1].scheduling_params.priority = SchedulingParams::Priority::UI; + + Entry* next = scheduler_->Next( + entries(), + BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED)); + EXPECT_EQ(&entries_[1], next); + MakeEntryActive(next); +} + +// When multiple UI priority entries exist, the next entry is selected based on +// cancel time and load balancing. +TEST_F(DownloadSchedulerImplTest, MultipleUIPriorityEntries) { + BuildScheduler(std::vector<DownloadClient>{DownloadClient::TEST, + DownloadClient::TEST_2}); + BuildDataEntries(4); + + // Client TEST: entry 0(UI priority), entry 1(UI priority, early cancel time). + // Client TEST_2: entry 2(UI priority), entry 3(high priority, early cancel + // time). Poll sequence: 1 -> 2 -> 0 -> 3. + for (auto& entry : entries_) { + entry.scheduling_params.priority = SchedulingParams::Priority::UI; + } + entries_[0].client = DownloadClient::TEST; + entries_[0].scheduling_params.cancel_time = base::Time::FromInternalValue(40); + entries_[1].client = DownloadClient::TEST; + entries_[1].scheduling_params.cancel_time = base::Time::FromInternalValue(20); + entries_[2].client = DownloadClient::TEST_2; + entries_[2].scheduling_params.cancel_time = base::Time::FromInternalValue(50); + entries_[3].client = DownloadClient::TEST_2; + entries_[3].scheduling_params.cancel_time = base::Time::FromInternalValue(20); + entries_[3].scheduling_params.priority = SchedulingParams::Priority::HIGH; + + // When device conditions are meet, UI priority entry with the earliest cancel + // time will be processed first. + Entry* next = scheduler_->Next( + entries(), + BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED)); + EXPECT_EQ(&entries_[1], next); + MakeEntryActive(next); + + // Next entry will be UI priority entry from another client. + next = scheduler_->Next( + entries(), + BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED)); + EXPECT_EQ(&entries_[2], next); + MakeEntryActive(next); + + next = scheduler_->Next( + entries(), + BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED)); + EXPECT_EQ(&entries_[0], next); + MakeEntryActive(next); + + next = scheduler_->Next( + entries(), + BuildDeviceStatus(BatteryStatus::CHARGING, NetworkStatus::UNMETERED)); + EXPECT_EQ(&entries_[3], next); + MakeEntryActive(next); +} + +// Ensures the reschedule logic works correctly, and we can pass the correct +// criteria to platform task scheduler. +TEST_F(DownloadSchedulerImplTest, Reschedule) { + InSequence s; + + BuildScheduler(std::vector<DownloadClient>{DownloadClient::TEST}); + BuildDataEntries(2); + entries_[0].client = DownloadClient::TEST; + entries_[1].client = DownloadClient::TEST; + + entries_[0].scheduling_params.battery_requirements = + SchedulingParams::BatteryRequirements::BATTERY_SENSITIVE; + entries_[0].scheduling_params.network_requirements = + SchedulingParams::NetworkRequirements::UNMETERED; + entries_[1].scheduling_params.battery_requirements = + SchedulingParams::BatteryRequirements::BATTERY_SENSITIVE; + entries_[1].scheduling_params.network_requirements = + SchedulingParams::NetworkRequirements::UNMETERED; + + Criteria criteria; + EXPECT_CALL(task_scheduler_, CancelTask(DownloadTaskType::DOWNLOAD_TASK)) + .RetiresOnSaturation(); + EXPECT_CALL(task_scheduler_, + ScheduleTask(DownloadTaskType::DOWNLOAD_TASK, + criteria.requires_unmetered_network, + criteria.requires_battery_charging, _, _)) + .RetiresOnSaturation(); + scheduler_->Reschedule(entries()); + + entries_[0].scheduling_params.battery_requirements = + SchedulingParams::BatteryRequirements::BATTERY_INSENSITIVE; + criteria.requires_battery_charging = false; + EXPECT_CALL(task_scheduler_, CancelTask(DownloadTaskType::DOWNLOAD_TASK)) + .RetiresOnSaturation(); + EXPECT_CALL(task_scheduler_, + ScheduleTask(DownloadTaskType::DOWNLOAD_TASK, + criteria.requires_unmetered_network, + criteria.requires_battery_charging, _, _)) + .RetiresOnSaturation(); + scheduler_->Reschedule(entries()); + + entries_[0].scheduling_params.network_requirements = + SchedulingParams::NetworkRequirements::NONE; + criteria.requires_unmetered_network = false; + EXPECT_CALL(task_scheduler_, CancelTask(DownloadTaskType::DOWNLOAD_TASK)) + .RetiresOnSaturation(); + EXPECT_CALL(task_scheduler_, + ScheduleTask(DownloadTaskType::DOWNLOAD_TASK, + criteria.requires_unmetered_network, + criteria.requires_battery_charging, _, _)) + .RetiresOnSaturation(); + scheduler_->Reschedule(entries()); +} + +} // namespace +} // namespace download diff --git a/chromium/components/download/internal/service_config_impl.cc b/chromium/components/download/internal/service_config_impl.cc new file mode 100644 index 00000000000..bb0e5ec0040 --- /dev/null +++ b/chromium/components/download/internal/service_config_impl.cc @@ -0,0 +1,24 @@ +// 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/service_config_impl.h" + +#include "base/time/time.h" +#include "components/download/internal/config.h" + +namespace download { + +ServiceConfigImpl::ServiceConfigImpl(Configuration* config) : config_(config) {} + +ServiceConfigImpl::~ServiceConfigImpl() = default; + +uint32_t ServiceConfigImpl::GetMaxScheduledDownloadsPerClient() const { + return config_->max_scheduled_downloads; +} + +const base::TimeDelta& ServiceConfigImpl::GetFileKeepAliveTime() const { + return config_->file_keep_alive_time; +} + +} // namespace download diff --git a/chromium/components/download/internal/service_config_impl.h b/chromium/components/download/internal/service_config_impl.h new file mode 100644 index 00000000000..15da514166e --- /dev/null +++ b/chromium/components/download/internal/service_config_impl.h @@ -0,0 +1,32 @@ +// 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. + +#ifndef COMPONENTS_DOWNLOAD_INTERNAL_SERVICE_CONFIG_IMPL_H_ +#define COMPONENTS_DOWNLOAD_INTERNAL_SERVICE_CONFIG_IMPL_H_ + +#include "base/macros.h" +#include "components/download/public/service_config.h" + +namespace download { + +struct Configuration; + +class ServiceConfigImpl : public ServiceConfig { + public: + explicit ServiceConfigImpl(Configuration* config); + ~ServiceConfigImpl() override; + + // ServiceConfig implementation. + uint32_t GetMaxScheduledDownloadsPerClient() const override; + const base::TimeDelta& GetFileKeepAliveTime() const override; + + private: + struct Configuration* config_; + + DISALLOW_COPY_AND_ASSIGN(ServiceConfigImpl); +}; + +} // namespace download + +#endif // COMPONENTS_DOWNLOAD_INTERNAL_SERVICE_CONFIG_IMPL_H_ diff --git a/chromium/components/download/internal/service_config_impl_unittest.cc b/chromium/components/download/internal/service_config_impl_unittest.cc new file mode 100644 index 00000000000..e093dec9de3 --- /dev/null +++ b/chromium/components/download/internal/service_config_impl_unittest.cc @@ -0,0 +1,23 @@ +// 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/service_config_impl.h" +#include "components/download/internal/config.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace download { + +TEST(ServiceConfigImplTest, TestApi) { + Configuration config; + ServiceConfigImpl impl(&config); + + config.max_scheduled_downloads = 7; + config.file_keep_alive_time = base::TimeDelta::FromSeconds(12); + + EXPECT_EQ(config.max_scheduled_downloads, + impl.GetMaxScheduledDownloadsPerClient()); + EXPECT_EQ(config.file_keep_alive_time, impl.GetFileKeepAliveTime()); +} + +} // namespace download diff --git a/chromium/components/download/internal/startup_status.cc b/chromium/components/download/internal/startup_status.cc new file mode 100644 index 00000000000..393e289a6a3 --- /dev/null +++ b/chromium/components/download/internal/startup_status.cc @@ -0,0 +1,28 @@ +// 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/startup_status.h" + +namespace download { + +StartupStatus::StartupStatus() = default; +StartupStatus::~StartupStatus() = default; + +void StartupStatus::Reset() { + driver_ok.reset(); + model_ok.reset(); + file_monitor_ok.reset(); +} + +bool StartupStatus::Complete() const { + return driver_ok.has_value() && model_ok.has_value() && + file_monitor_ok.has_value(); +} + +bool StartupStatus::Ok() const { + DCHECK(Complete()); + return driver_ok.value() && model_ok.value() && file_monitor_ok.value(); +} + +} // namespace download diff --git a/chromium/components/download/internal/startup_status.h b/chromium/components/download/internal/startup_status.h new file mode 100644 index 00000000000..754428afad9 --- /dev/null +++ b/chromium/components/download/internal/startup_status.h @@ -0,0 +1,40 @@ +// 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. + +#ifndef COMPONENTS_DOWNLOAD_STARTUP_STATUS_H_ +#define COMPONENTS_DOWNLOAD_STARTUP_STATUS_H_ + +#include "base/macros.h" +#include "base/optional.h" + +namespace download { + +// Helper struct to track the initialization status of various Controller +// internal components. +struct StartupStatus { + StartupStatus(); + ~StartupStatus(); + + base::Optional<bool> driver_ok; + base::Optional<bool> model_ok; + base::Optional<bool> file_monitor_ok; + + // Resets all startup state tracking. + void Reset(); + + // Whether or not all components have finished initialization. Note that this + // does not mean that all components were initialized successfully. + bool Complete() const; + + // Whether or not all components have initialized successfully. Should only + // be called if Complete() is true. + bool Ok() const; + + private: + DISALLOW_COPY_AND_ASSIGN(StartupStatus); +}; + +} // namespace download + +#endif // COMPONENTS_DOWNLOAD_STARTUP_STATUS_H_ diff --git a/chromium/components/download/internal/stats.cc b/chromium/components/download/internal/stats.cc new file mode 100644 index 00000000000..5b4f12c52a3 --- /dev/null +++ b/chromium/components/download/internal/stats.cc @@ -0,0 +1,310 @@ +// 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/stats.h" + +#include <map> + +#include "base/files/file_enumerator.h" +#include "base/files/file_util.h" +#include "base/metrics/histogram_functions.h" +#include "base/metrics/histogram_macros.h" +#include "components/download/internal/startup_status.h" + +namespace download { +namespace stats { +namespace { + +// The maximum tracked file size in KB, larger files will fall into overflow +// bucket. +const int64_t kMaxFileSizeKB = 4 * 1024 * 1024; /* 4GB */ + +// Converts DownloadTaskType to histogram suffix. +// Should maps to suffix string in histograms.xml. +std::string TaskTypeToHistogramSuffix(DownloadTaskType task_type) { + switch (task_type) { + case DownloadTaskType::DOWNLOAD_TASK: + return "DownloadTask"; + case DownloadTaskType::CLEANUP_TASK: + return "CleanUpTask"; + } + NOTREACHED(); + return std::string(); +} + +// Converts Entry::State to histogram suffix. +// Should maps to suffix string in histograms.xml. +std::string EntryStateToHistogramSuffix(Entry::State state) { + std::string suffix; + switch (state) { + case Entry::State::NEW: + return "New"; + case Entry::State::AVAILABLE: + return "Available"; + case Entry::State::ACTIVE: + return "Active"; + case Entry::State::PAUSED: + return "Paused"; + case Entry::State::COMPLETE: + return "Complete"; + case Entry::State::COUNT: + break; + } + NOTREACHED(); + return std::string(); +} + +// Converts DownloadClient to histogram suffix. +// Should maps to suffix string in histograms.xml. +std::string ClientToHistogramSuffix(DownloadClient client) { + switch (client) { + case DownloadClient::TEST: + case DownloadClient::TEST_2: + case DownloadClient::TEST_3: + case DownloadClient::INVALID: + return "__Test__"; + case DownloadClient::OFFLINE_PAGE_PREFETCH: + return "OfflinePage"; + case DownloadClient::BOUNDARY: + NOTREACHED(); + break; + } + NOTREACHED(); + return std::string(); +} + +// Converts CompletionType to histogram suffix. +// Should maps to suffix string in histograms.xml. +std::string CompletionTypeToHistogramSuffix(CompletionType type) { + switch (type) { + case CompletionType::SUCCEED: + return "Succeed"; + case CompletionType::FAIL: + return "Fail"; + case CompletionType::ABORT: + return "Abort"; + case CompletionType::TIMEOUT: + return "Timeout"; + case CompletionType::UNKNOWN: + return "Unknown"; + case CompletionType::CANCEL: + return "Cancel"; + case CompletionType::COUNT: + NOTREACHED(); + } + NOTREACHED(); + return std::string(); +} + +// Converts FileCleanupReason to histogram suffix. +// Should maps to suffix string in histograms.xml. +std::string FileCleanupReasonToHistogramSuffix(FileCleanupReason reason) { + switch (reason) { + case FileCleanupReason::TIMEOUT: + return "Timeout"; + case FileCleanupReason::ORPHANED: + return "Orphaned"; + case FileCleanupReason::UNKNOWN: + return "Unknown"; + case FileCleanupReason::HARD_RECOVERY: + return "HardRecovery"; + case FileCleanupReason::COUNT: + NOTREACHED(); + } + NOTREACHED(); + return std::string(); +} + +// Helper method to log StartUpResult. +void LogStartUpResult(bool in_recovery, StartUpResult result) { + if (in_recovery) { + base::UmaHistogramEnumeration("Download.Service.StartUpStatus.Recovery", + result, StartUpResult::COUNT); + } else { + base::UmaHistogramEnumeration( + "Download.Service.StartUpStatus.Initialization", result, + StartUpResult::COUNT); + } +} + +// Helper method to log the number of entries under a particular state. +void LogDatabaseRecords(Entry::State state, uint32_t record_count) { + std::string name("Download.Service.Db.Records"); + name.append(".").append(EntryStateToHistogramSuffix(state)); + base::UmaHistogramCustomCounts(name, record_count, 1, 500, 50); +} + +} // namespace + +void LogControllerStartupStatus(bool in_recovery, const StartupStatus& status) { + DCHECK(status.Complete()); + + // Total counts for general success/failure rate. + LogStartUpResult(in_recovery, status.Ok() ? StartUpResult::SUCCESS + : StartUpResult::FAILURE); + + // Failure reasons. + if (!status.driver_ok.value()) + LogStartUpResult(in_recovery, StartUpResult::FAILURE_REASON_DRIVER); + if (!status.model_ok.value()) + LogStartUpResult(in_recovery, StartUpResult::FAILURE_REASON_MODEL); + if (!status.file_monitor_ok.value()) + LogStartUpResult(in_recovery, StartUpResult::FAILURE_REASON_FILE_MONITOR); +} + +void LogServiceApiAction(DownloadClient client, ServiceApiAction action) { + // Total count for each action. + std::string name("Download.Service.Request.ClientAction"); + base::UmaHistogramEnumeration(name, action, ServiceApiAction::COUNT); + + // Total count for each action with client suffix. + name.append(".").append(ClientToHistogramSuffix(client)); + base::UmaHistogramEnumeration(name, action, ServiceApiAction::COUNT); +} + +void LogStartDownloadResult(DownloadClient client, + DownloadParams::StartResult result) { + // Total count for each start result. + std::string name("Download.Service.Request.StartResult"); + base::UmaHistogramEnumeration(name, result, + DownloadParams::StartResult::COUNT); + + // Total count for each client result with client suffix. + name.append(".").append(ClientToHistogramSuffix(client)); + base::UmaHistogramEnumeration(name, result, + DownloadParams::StartResult::COUNT); +} + +void LogStartDownloadResponse(DownloadClient client, + Client::ShouldDownload should_download) { + // Total count for each start response. + std::string name("Download.Service.Request.StartResponse"); + base::UmaHistogramEnumeration(name, should_download, + Client::ShouldDownload::COUNT); + + // Total count for each client response with client suffix. + name.append(".").append(ClientToHistogramSuffix(client)); + base::UmaHistogramEnumeration(name, should_download, + Client::ShouldDownload::COUNT); +} + +void LogDownloadParams(const DownloadParams& params) { + UMA_HISTOGRAM_ENUMERATION("Download.Service.Request.BatteryRequirement", + params.scheduling_params.battery_requirements, + SchedulingParams::BatteryRequirements::COUNT); + UMA_HISTOGRAM_ENUMERATION("Download.Service.Request.NetworkRequirement", + params.scheduling_params.network_requirements, + SchedulingParams::NetworkRequirements::COUNT); + UMA_HISTOGRAM_ENUMERATION("Download.Service.Request.Priority", + params.scheduling_params.priority, + SchedulingParams::Priority::COUNT); +} + +void LogRecoveryOperation(Entry::State to_state) { + UMA_HISTOGRAM_ENUMERATION("Download.Service.Recovery", to_state, + Entry::State::COUNT); +} + +void LogDownloadCompletion(CompletionType type, + const base::TimeDelta& time_span, + uint64_t file_size_bytes) { + // Records completion type. + UMA_HISTOGRAM_ENUMERATION("Download.Service.Finish.Type", type, + CompletionType::COUNT); + + // TODO(xingliu): Use DownloadItem::GetStartTime and DownloadItem::GetEndTime + // to record the completion time to histogram "Download.Service.Finish.Time". + // Also propagates and records the mime type here. + + // Records the file size. + std::string name("Download.Service.Finish.FileSize"); + uint64_t file_size_kb = file_size_bytes / 1024; + base::UmaHistogramCustomCounts(name, file_size_kb, 1, kMaxFileSizeKB, 50); + + name.append(".").append(CompletionTypeToHistogramSuffix(type)); + base::UmaHistogramCustomCounts(name, file_size_kb, 1, kMaxFileSizeKB, 50); +} + +void LogModelOperationResult(ModelAction action, bool success) { + if (success) { + UMA_HISTOGRAM_ENUMERATION("Download.Service.Db.Operation.Success", action, + ModelAction::COUNT); + } else { + UMA_HISTOGRAM_ENUMERATION("Download.Service.Db.Operation.Failure", action, + ModelAction::COUNT); + } +} + +void LogEntries(std::map<Entry::State, uint32_t>& entries_count) { + uint32_t total_records = 0; + for (const auto& entry_count : entries_count) + total_records += entry_count.second; + + // Total number of records in database. + base::UmaHistogramCustomCounts("Download.Service.Db.Records", total_records, + 1, 500, 50); + + // Number of records for each Entry::State. + for (Entry::State state = Entry::State::NEW; state != Entry::State::COUNT; + state = (Entry::State)((int)(state) + 1)) { + LogDatabaseRecords(state, entries_count[state]); + } +} + +void LogScheduledTaskStatus(DownloadTaskType task_type, + ScheduledTaskStatus status) { + std::string name("Download.Service.TaskScheduler.Status"); + base::UmaHistogramEnumeration(name, status, ScheduledTaskStatus::COUNT); + + name.append(".").append(TaskTypeToHistogramSuffix(task_type)); + base::UmaHistogramEnumeration(name, status, ScheduledTaskStatus::COUNT); +} + +void LogsFileDirectoryCreationError(base::File::Error error) { + // Maps to histogram enum PlatformFileError. + UMA_HISTOGRAM_ENUMERATION("Download.Service.Files.DirCreationError", -error, + -base::File::Error::FILE_ERROR_MAX); +} + +void LogFileCleanupStatus(FileCleanupReason reason, + int succeeded_cleanups, + int failed_cleanups, + int external_cleanups) { + std::string name("Download.Service.Files.CleanUp.Success"); + base::UmaHistogramCounts100(name, succeeded_cleanups); + name.append(".").append(FileCleanupReasonToHistogramSuffix(reason)); + base::UmaHistogramCounts100(name, succeeded_cleanups); + + name = "Download.Service.Files.CleanUp.Failure"; + base::UmaHistogramCounts100(name, failed_cleanups); + name.append(".").append(FileCleanupReasonToHistogramSuffix(reason)); + base::UmaHistogramCounts100(name, failed_cleanups); + + name = "Download.Service.Files.CleanUp.External"; + base::UmaHistogramCounts100(name, external_cleanups); + name.append(".").append(FileCleanupReasonToHistogramSuffix(reason)); + base::UmaHistogramCounts100(name, external_cleanups); +} + +void LogFileLifeTime(const base::TimeDelta& file_life_time) { + UMA_HISTOGRAM_CUSTOM_TIMES("Download.Service.Files.LifeTime", file_life_time, + base::TimeDelta::FromSeconds(1), + base::TimeDelta::FromDays(8), 100); +} + +void LogFileDirDiskUtilization(int64_t total_disk_space, + int64_t free_disk_space, + int64_t files_size) { + UMA_HISTOGRAM_PERCENTAGE("Download.Service.Files.FreeDiskSpace", + (free_disk_space * 100) / total_disk_space); + UMA_HISTOGRAM_PERCENTAGE("Download.Service.Files.DiskUsed", + (files_size * 100) / total_disk_space); +} + +void LogFilePathRenamed(bool renamed) { + UMA_HISTOGRAM_BOOLEAN("Download.Service.Files.PathRenamed", renamed); +} + +} // namespace stats +} // namespace download diff --git a/chromium/components/download/internal/stats.h b/chromium/components/download/internal/stats.h new file mode 100644 index 00000000000..e100d35189d --- /dev/null +++ b/chromium/components/download/internal/stats.h @@ -0,0 +1,191 @@ +// 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. + +#ifndef COMPONENTS_DOWNLOAD_INTERNAL_STATS_H_ +#define COMPONENTS_DOWNLOAD_INTERNAL_STATS_H_ + +#include "base/files/file.h" +#include "components/download/internal/controller.h" +#include "components/download/internal/entry.h" +#include "components/download/public/clients.h" +#include "components/download/public/download_params.h" +#include "components/download/public/download_task_types.h" + +namespace download { + +struct StartupStatus; + +namespace stats { + +// Please follow the following rules for all enums: +// 1. Keep them in sync with the corresponding entry in enums.xml. +// 2. Treat them as append only. +// 3. Do not remove any enums. Only mark them as deprecated. + +// Enum used to track download service start up result and failure reasons. +// Most of the fields should map to StartupStatus. +// Failure reasons are not mutually exclusive. +enum class StartUpResult { + // Download service successfully started. + SUCCESS = 0, + + // Download service start up failed. + FAILURE = 1, + + // Download driver is not ready. + FAILURE_REASON_DRIVER = 2, + + // Database layer failed to initialized. + FAILURE_REASON_MODEL = 3, + + // File monitor failed to start. + FAILURE_REASON_FILE_MONITOR = 4, + + // The count of entries for the enum. + COUNT = 5, +}; + +// Enum used by UMA metrics to track which actions a Client is taking on the +// service. +enum class ServiceApiAction { + // Represents a call to DownloadService::StartDownload. + START_DOWNLOAD = 0, + + // Represents a call to DownloadService::PauseDownload. + PAUSE_DOWNLOAD = 1, + + // Represents a call to DownloadService::ResumeDownload. + RESUME_DOWNLOAD = 2, + + // Represents a call to DownloadService::CancelDownload. + CANCEL_DOWNLOAD = 3, + + // Represents a call to DownloadService::ChangeCriteria. + CHANGE_CRITERIA = 4, + + // The count of entries for the enum. + COUNT = 5, +}; + +// Enum used by UMA metrics to tie to specific actions taken on a Model. This +// can be used to track failure events. +enum class ModelAction { + // Represents an attempt to initialize the Model. + INITIALIZE = 0, + + // Represents an attempt to add an Entry to the Model. + ADD = 1, + + // Represents an attempt to update an Entry in the Model. + UPDATE = 2, + + // Represents an attempt to remove an Entry from the Model. + REMOVE = 3, + + // The count of entries for the enum. + COUNT = 4, +}; + +// Enum used by UMA metrics to log the status of scheduled tasks. +enum class ScheduledTaskStatus { + // Startup failed and the task was not run. + ABORTED_ON_FAILED_INIT = 0, + + // OnStopScheduledTask() was received before the task could be fired. + CANCELLED_ON_STOP = 1, + + // Callback was run successfully after completion of the task. + COMPLETED_NORMALLY = 2, + + // The count of entries for the enum. + COUNT = 3, +}; + +// Enum used by UMA metrics to track various types of cleanup actions taken by +// the service. +enum class FileCleanupReason { + // The file was deleted by the service after timeout. + TIMEOUT = 0, + + // The database entry for the file was found not associated with any + // registered client. + ORPHANED = 1, + + // At startup, the file was found not being associated with any model entry or + // driver entry. + UNKNOWN = 2, + + // We're trying to remove all files as part of a hard recovery attempt. + HARD_RECOVERY = 3, + + // The count of entries for the enum. + COUNT = 4, +}; + +// Logs the results of starting up the Controller. Will log each failure reason +// if |status| contains more than one initialization failure. +void LogControllerStartupStatus(bool in_recovery, const StartupStatus& status); + +// Logs an action taken on the service API. +void LogServiceApiAction(DownloadClient client, ServiceApiAction action); + +// Logs the result of a StartDownload() attempt on the service. +void LogStartDownloadResult(DownloadClient client, + DownloadParams::StartResult result); + +// Logs the client response to StartDownload() attempt on the service. +void LogStartDownloadResponse(DownloadClient client, + download::Client::ShouldDownload should_download); + +// Logs the download parameters when StartDownload() is called. +void LogDownloadParams(const DownloadParams& params); + +// Logs recovery operations that happened when we had to move from one state +// to another on startup. +void LogRecoveryOperation(Entry::State to_state); + +// Logs download completion event, download time, and the file size. +void LogDownloadCompletion(CompletionType type, + const base::TimeDelta& time_span, + uint64_t file_size_bytes); + +// Logs statistics about the result of a model operation. Used to track failure +// cases. +void LogModelOperationResult(ModelAction action, bool success); + +// Logs the total number of all entries, and the number of entries in each +// state after the model is initialized. +void LogEntries(std::map<Entry::State, uint32_t>& entries_count); + +// Log statistics about the status of a TaskFinishedCallback. +void LogScheduledTaskStatus(DownloadTaskType task_type, + ScheduledTaskStatus status); + +// Logs download files directory creation error. +void LogsFileDirectoryCreationError(base::File::Error error); + +// Logs statistics about the reasons of a file cleanup. +void LogFileCleanupStatus(FileCleanupReason reason, + int succeeded_cleanups, + int failed_cleanups, + int external_cleanups); + +// Logs the file life time for successfully completed download. +void LogFileLifeTime(const base::TimeDelta& file_life_time); + +// Logs the total disk space utilized by download files. +// This includes the total size of all the files in |file_dir|. +// This function is costly and should be called only once. +void LogFileDirDiskUtilization(int64_t total_disk_space, + int64_t free_disk_space, + int64_t files_size); + +// Logs if the final download file path is different from the requested file +// path. +void LogFilePathRenamed(bool renamed); + +} // namespace stats +} // namespace download + +#endif // COMPONENTS_DOWNLOAD_INTERNAL_STATS_H_ diff --git a/chromium/components/download/internal/store.h b/chromium/components/download/internal/store.h index 04a39bffcc6..1d1e5686687 100644 --- a/chromium/components/download/internal/store.h +++ b/chromium/components/download/internal/store.h @@ -10,8 +10,6 @@ #include <vector> #include "base/callback_forward.h" -#include "base/memory/ref_counted.h" -#include "base/sequenced_task_runner.h" namespace download { @@ -36,9 +34,8 @@ class Store { // Store. virtual void Initialize(InitCallback callback) = 0; - // Destroyes the store and asynchronously returns whether or not that - // destruction was successful. - virtual void Destroy(StoreCallback callback) = 0; + // Destroys the underlying store and attempts to re-initialize. + virtual void HardRecover(StoreCallback callback) = 0; // Adds or updates |entry| in this Store asynchronously and returns whether or // not that was successful. diff --git a/chromium/components/download/internal/test/BUILD.gn b/chromium/components/download/internal/test/BUILD.gn index 87032dcfd7d..99011eef401 100644 --- a/chromium/components/download/internal/test/BUILD.gn +++ b/chromium/components/download/internal/test/BUILD.gn @@ -8,10 +8,22 @@ source_set("test_support") { testonly = true sources = [ + "download_params_utils.cc", + "download_params_utils.h", + "empty_client.cc", + "empty_client.h", "entry_utils.cc", "entry_utils.h", + "mock_client.cc", + "mock_client.h", + "mock_controller.cc", + "mock_controller.h", "mock_model_client.cc", "mock_model_client.h", + "noop_store.cc", + "noop_store.h", + "test_device_status_listener.cc", + "test_device_status_listener.h", "test_download_driver.cc", "test_download_driver.h", "test_store.cc", diff --git a/chromium/components/download/public/BUILD.gn b/chromium/components/download/public/BUILD.gn index 5f013fd35b6..f51a715b16d 100644 --- a/chromium/components/download/public/BUILD.gn +++ b/chromium/components/download/public/BUILD.gn @@ -14,8 +14,11 @@ source_set("public") { "download_params.cc", "download_params.h", "download_service.h", + "download_task_types.h", "features.cc", "features.h", + "service_config.h", + "task_scheduler.h", ] deps = [ @@ -28,3 +31,22 @@ source_set("public") { "//url", ] } + +if (is_android) { + android_library("public_java") { + srcjar_deps = [ ":jni_enums" ] + + deps = [ + "//base:base_java", + "//third_party/android_tools:android_support_annotations_java", + ] + } + + java_cpp_enum("jni_enums") { + visibility = [ "*" ] + + sources = [ + "download_task_types.h", + ] + } +} diff --git a/chromium/components/download/public/client.h b/chromium/components/download/public/client.h index 28264e7b97c..518d7e223b3 100644 --- a/chromium/components/download/public/client.h +++ b/chromium/components/download/public/client.h @@ -22,18 +22,56 @@ class Client { // Used by OnDownloadStarted to determine whether or not the DownloadService // should continue downloading the file or abort the attempt. enum class ShouldDownload { + // Continue to download the file. CONTINUE, + + // Abort the download. ABORT, + + // The count of entries for the enum. + COUNT, + }; + + // Used by OnDownloadFailed to determine the reason of the abort. + enum class FailureReason { + // Used when the download has been aborted after reaching a threshold where + // we decide it is not worth attempting to start again. This could be + // either due to a specific number of failed retry attempts or a specific + // number of wasted bytes due to the download restarting. + NETWORK, + + // Used when the download was not completed before the + // DownloadParams::cancel_after timeout. + TIMEDOUT, + + // Used when the download was cancelled by the Client. + CANCELLED, + + // Used when the download was aborted by the Client in response to the + // download starting (see OnDownloadStarted()). + ABORTED, + + // Used when the failure reason is unknown. This generally means that we + // detect that the download failed during a restart, but aren't sure exactly + // what triggered the failure before shutdown. + UNKNOWN, }; virtual ~Client() = default; // Called when the DownloadService is initialized and ready to be interacted // with. |outstanding_download_guids| is a list of all downloads the - // DownloadService is aware of that are associated with this Client. + // DownloadService is aware of that are associated with this Client. If + // |state_lost| is |true|, the service ran into an error initializing and had + // to destroy all internal persisted state. At this point any saved files + // might not be available and any previously scheduled downloads are gone. virtual void OnServiceInitialized( + bool state_lost, const std::vector<std::string>& outstanding_download_guids) = 0; + // Called when the DownloadService fails to initialize and should not be used. + virtual void OnServiceUnavailable() = 0; + // Return whether or not the download should be aborted (potentially in // response to |headers|). The download will be downloading at the time this // call is made. @@ -51,18 +89,11 @@ class Client { virtual void OnDownloadUpdated(const std::string& guid, uint64_t bytes_downloaded) = 0; - // TODO(dtrainor): Expose a useful error message with the failed download. - virtual void OnDownloadFailed(const std::string& guid) = 0; - - // Called when the download was not completed before the - // DownloadParams::cancel_after timeout. - virtual void OnDownloadTimedOut(const std::string& guid) = 0; - - // Called when the download has been aborted after reaching a treshold where - // we decide it is not worth attempting to start again. This could be either - // due to a specific number of failed retry attempts or a specific number of - // wasted bytes due to the download restarting. - virtual void OnDownloadAborted(const std::string& guid) = 0; + // Called when a download failed. Check FailureReason for a list of possible + // reasons why this failure occurred. Note that this will also be called for + // cancelled downloads. + virtual void OnDownloadFailed(const std::string& guid, + FailureReason reason) = 0; // Called when a download has been successfully completed. After this call // the download entry will be purged from the database. The file will be diff --git a/chromium/components/download/public/clients.h b/chromium/components/download/public/clients.h index a464d212017..3939aea2638 100644 --- a/chromium/components/download/public/clients.h +++ b/chromium/components/download/public/clients.h @@ -5,6 +5,11 @@ #ifndef COMPONENTS_DOWNLOAD_PUBLIC_CLIENTS_H_ #define COMPONENTS_DOWNLOAD_PUBLIC_CLIENTS_H_ +#include <map> +#include <memory> + +#include "components/download/public/client.h" + namespace download { // A list of all clients that are able to make download requests through the @@ -15,19 +20,23 @@ namespace download { // but also to make sure the underlying database properly associates each // download with the right client. enum class DownloadClient { - // Represents an uninitialized DownloadClient variable. - INVALID = 0, - // Test client values. Meant to be used by the testing framework and not // production code. Callers will be unable to access the DownloadService with // these test APIs. - TEST = 1, + TEST = -1, + TEST_2 = -2, + TEST_3 = -3, - OFFLINE_PAGE_PREFETCH = 2, + // Represents an uninitialized DownloadClient variable. + INVALID = 0, - BOUNDARY = 3, + OFFLINE_PAGE_PREFETCH = 1, + + BOUNDARY = 2, }; +using DownloadClientMap = std::map<DownloadClient, std::unique_ptr<Client>>; + } // namespace download #endif // COMPONENTS_DOWNLOAD_PUBLIC_CLIENTS_H_ diff --git a/chromium/components/download/public/download_params.cc b/chromium/components/download/public/download_params.cc index f24d467b763..2c3f10652a2 100644 --- a/chromium/components/download/public/download_params.cc +++ b/chromium/components/download/public/download_params.cc @@ -9,10 +9,17 @@ namespace download { SchedulingParams::SchedulingParams() - : priority(Priority::DEFAULT), + : cancel_time(base::Time::Max()), + priority(Priority::DEFAULT), network_requirements(NetworkRequirements::NONE), battery_requirements(BatteryRequirements::BATTERY_INSENSITIVE) {} +bool SchedulingParams::operator==(const SchedulingParams& rhs) const { + return network_requirements == rhs.network_requirements && + battery_requirements == rhs.battery_requirements && + priority == rhs.priority && cancel_time == rhs.cancel_time; +} + RequestParams::RequestParams() : method("GET") {} DownloadParams::DownloadParams() : client(DownloadClient::INVALID) {} diff --git a/chromium/components/download/public/download_params.h b/chromium/components/download/public/download_params.h index dcf25b41986..217718efbc9 100644 --- a/chromium/components/download/public/download_params.h +++ b/chromium/components/download/public/download_params.h @@ -30,6 +30,9 @@ struct SchedulingParams { // The download can occur only if the network isn't metered. UNMETERED = 2, + + // Last value of the enum. + COUNT = 3, }; enum class BatteryRequirements { @@ -41,6 +44,9 @@ struct SchedulingParams { // The download can only occur when charging or in optimal battery // conditions. BATTERY_SENSITIVE = 1, + + // Last value of the enum. + COUNT = 2, }; enum class Priority { @@ -61,13 +67,19 @@ struct SchedulingParams { // The default priority for all tasks unless overridden. DEFAULT = NORMAL, + + // Last value of the enum. + COUNT = 4, }; SchedulingParams(); SchedulingParams(const SchedulingParams& other) = default; ~SchedulingParams() = default; + bool operator==(const SchedulingParams& rhs) const; + // Cancel the download after this time. Will cancel in-progress downloads. + // base::Time::Max() if not specified. base::Time cancel_time; // The suggested priority. Non-UI priorities may not be honored by the @@ -102,12 +114,27 @@ struct DownloadParams { // The DownloadService has too many downloads. Backoff and retry. BACKOFF, - // Failed to create the download. Invalid input parameters. - BAD_PARAMETERS, + // The DownloadService has no knowledge of the DownloadClient associated + // with this request. + UNEXPECTED_CLIENT, + + // Failed to create the download. The guid is already in use. + UNEXPECTED_GUID, + + // The download was cancelled by the Client while it was being persisted. + CLIENT_CANCELLED, + + // The DownloadService was unable to accept and persist this download due to + // an internal error like the underlying DB store failing to write to disk. + INTERNAL_ERROR, // TODO(dtrainor): Add more error codes. + // The count of entries for the enum. + COUNT, }; + using StartCallback = base::Callback<void(const std::string&, StartResult)>; + DownloadParams(); DownloadParams(const DownloadParams& other); ~DownloadParams(); @@ -116,11 +143,13 @@ struct DownloadParams { DownloadClient client; // A unique GUID that represents this download. See |base::GenerateGUID()|. + // TODO(xingliu): guid in content download must be upper case, see + // http://crbug.com/734818. std::string guid; // A callback that will be notified if this download has been accepted and // persisted by the DownloadService. - base::Callback<void(const DownloadParams&, StartResult)> callback; + StartCallback callback; // The parameters that determine under what device conditions this download // will occur. diff --git a/chromium/components/download/public/download_service.h b/chromium/components/download/public/download_service.h index f0733de6ff4..fdf8ac512bf 100644 --- a/chromium/components/download/public/download_service.h +++ b/chromium/components/download/public/download_service.h @@ -5,6 +5,7 @@ #ifndef COMPONENTS_DOWNLOAD_PUBLIC_DOWNLOAD_SERVICE_H_ #define COMPONENTS_DOWNLOAD_PUBLIC_DOWNLOAD_SERVICE_H_ +#include <memory> #include <string> #include "base/files/file_path.h" @@ -12,12 +13,18 @@ #include "base/memory/ptr_util.h" #include "base/memory/ref_counted.h" #include "base/sequenced_task_runner.h" +#include "components/download/public/clients.h" +#include "components/download/public/download_task_types.h" #include "components/keyed_service/core/keyed_service.h" namespace download { +class Client; struct DownloadParams; struct SchedulingParams; +class ServiceConfig; + +using TaskFinishedCallback = base::Callback<void(bool)>; // A service responsible for helping facilitate the scheduling and downloading // of file content from the web. See |DownloadParams| for more details on the @@ -27,21 +34,50 @@ struct SchedulingParams; // feature requesting a download will have to implement a download::Client // interface so this class knows who to contact when a download completes after // a process restart. +// See the embedder specific factories for creation options. class DownloadService : public KeyedService { public: - // |storage_dir| is a path to where all the local storage will be. This will - // hold the internal database as well as any temporary files on disk. If this - // is an empty path, the service will not persist any information to disk and - // will act as an in-memory only service (this means no auto-retries after - // restarts, no files written on completion, etc.). - // |background_task_runner| will be used for all disk reads and writes. - static DownloadService* Create( - const base::FilePath& storage_dir, - const scoped_refptr<base::SequencedTaskRunner>& background_task_runner); + // The current status of the Service. + enum class ServiceStatus { + // The service is in the process of initializing and should not be used yet. + // All registered Clients will be notified via + // Client::OnServiceInitialized() once the service is ready. + STARTING_UP = 0, + + // The service is ready and available for use. + READY = 1, + + // The service is unavailable. This is typically due to an unrecoverable + // error on some internal component like the persistence layer. + UNAVAILABLE = 2, + }; + + // Returns useful configuration information about the DownloadService. + virtual const ServiceConfig& GetConfig() = 0; + + // Callback method to run by the service when a pre-scheduled task starts. + // This method is invoked on main thread and while it is running, the system + // holds a wakelock which is not released until either the |callback| is run + // or OnStopScheduledTask is invoked by the system. Do not call this method + // directly. + virtual void OnStartScheduledTask(DownloadTaskType task_type, + const TaskFinishedCallback& callback) = 0; + + // Callback method to run by the service if the system decides to stop the + // task. Returns true if the task needs to be rescheduled. Any pending + // TaskFinishedCallback should be reset after this call. Do not call this + // method directly. + virtual bool OnStopScheduledTask(DownloadTaskType task_type) = 0; + + // Whether or not the DownloadService is currently available, initialized + // successfully, and ready to be used. + virtual ServiceStatus GetStatus() = 0; // Sends the download to the service. A callback to // |DownloadParams::callback| will be triggered once the download has been - // persisted and saved in the service + // persisted and saved in the service. + // TODO(xingliu): Remove the limitation of upper case guid in + // |download_params|, see http://crbug.com/734818. virtual void StartDownload(const DownloadParams& download_params) = 0; // Allows any feature to pause or resume downloads at will. Paused downloads diff --git a/chromium/components/download/public/download_task_types.h b/chromium/components/download/public/download_task_types.h new file mode 100644 index 00000000000..a26bc013743 --- /dev/null +++ b/chromium/components/download/public/download_task_types.h @@ -0,0 +1,22 @@ +// 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. + +#ifndef COMPONENTS_DOWNLOAD_PUBLIC_DOWNLOAD_TASK_TYPES_H_ +#define COMPONENTS_DOWNLOAD_PUBLIC_DOWNLOAD_TASK_TYPES_H_ + +namespace download { + +// A Java counterpart will be generated for this enum. +// GENERATED_JAVA_ENUM_PACKAGE: org.chromium.components.download +enum class DownloadTaskType { + // Task to invoke download service to take various download actions. + DOWNLOAD_TASK = 0, + + // Task to remove unnecessary files from the system. + CLEANUP_TASK = 1, +}; + +} // namespace download + +#endif // COMPONENTS_DOWNLOAD_PUBLIC_DOWNLOAD_TASK_TYPES_H_ diff --git a/chromium/components/download/public/service_config.h b/chromium/components/download/public/service_config.h new file mode 100644 index 00000000000..de51fbd5dd9 --- /dev/null +++ b/chromium/components/download/public/service_config.h @@ -0,0 +1,37 @@ +// 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. + +#ifndef COMPONENTS_DOWNLOAD_PUBLIC_SERVICE_CONFIG_H_ +#define COMPONENTS_DOWNLOAD_PUBLIC_SERVICE_CONFIG_H_ + +#include <stdint.h> + +namespace base { +class TimeDelta; +} // namespace base + +namespace download { + +// Contains the configuration used by this DownloadService for internal download +// operations. Meant to be used by Clients for any tweaking they might want to +// do based on the configuration parameters. +class ServiceConfig { + public: + virtual ~ServiceConfig() = default; + + // The maximum number of downloads that can be outstanding per Client. Any + // Client attempting to schedule more downloads than this limit will receive a + // DownloadParams::StartResult::BACKOFF return value. + virtual uint32_t GetMaxScheduledDownloadsPerClient() const = 0; + + // Returns the minimum amount of time the DownloadService will wait before + // automatically erasing any files that remain on disk in the same place with + // the same name for a completed download. It is up to the Client to move or + // consume the file before this time limit is reached. + virtual const base::TimeDelta& GetFileKeepAliveTime() const = 0; +}; + +} // namespace download + +#endif // COMPONENTS_DOWNLOAD_PUBLIC_SERVICE_CONFIG_H_ diff --git a/chromium/components/download/public/task_scheduler.h b/chromium/components/download/public/task_scheduler.h new file mode 100644 index 00000000000..ed436432b99 --- /dev/null +++ b/chromium/components/download/public/task_scheduler.h @@ -0,0 +1,36 @@ +// 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. + +#ifndef COMPONENTS_DOWNLOAD_PUBLIC_TASK_SCHEDULER_H_ +#define COMPONENTS_DOWNLOAD_PUBLIC_TASK_SCHEDULER_H_ + +#include "components/download/public/download_task_types.h" + +namespace download { + +// A helper class backed by system APIs to schedule jobs in the background. The +// tasks can run independently of each other as long as they have different +// |task_type|. Scheduling another task of same |task_type| before the task is +// fired will cancel the previous task. +class TaskScheduler { + public: + // Schedules a task with the operating system. The system has the liberty of + // firing the task any time between |window_start_time_seconds| and + // |window_end_time_seconds|. If the trigger conditions are not met, the + // behavior is unknown. + virtual void ScheduleTask(DownloadTaskType task_type, + bool require_unmetered_network, + bool require_charging, + long window_start_time_seconds, + long window_end_time_seconds) = 0; + + // Cancels a pre-scheduled task of type |task_type|. + virtual void CancelTask(DownloadTaskType task_type) = 0; + + virtual ~TaskScheduler() {} +}; + +} // namespace download + +#endif // COMPONENTS_DOWNLOAD_PUBLIC_TASK_SCHEDULER_H_ |