| // Copyright 2016 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/offline_pages/core/offline_page_model_impl.h" |
| |
| #include <algorithm> |
| #include <limits> |
| |
| #include "base/bind.h" |
| #include "base/files/file_util.h" |
| #include "base/location.h" |
| #include "base/logging.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/rand_util.h" |
| #include "base/sequenced_task_runner.h" |
| #include "base/strings/string16.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "base/time/clock.h" |
| #include "base/time/time.h" |
| #include "components/offline_pages/core/archive_manager.h" |
| #include "components/offline_pages/core/client_namespace_constants.h" |
| #include "components/offline_pages/core/client_policy_controller.h" |
| #include "components/offline_pages/core/offline_page_item.h" |
| #include "components/offline_pages/core/offline_page_model_query.h" |
| #include "components/offline_pages/core/offline_page_storage_manager.h" |
| #include "url/gurl.h" |
| |
| using ArchiverResult = offline_pages::OfflinePageArchiver::ArchiverResult; |
| using ClearStorageCallback = |
| offline_pages::OfflinePageStorageManager::ClearStorageCallback; |
| using ClearStorageResult = |
| offline_pages::OfflinePageStorageManager::ClearStorageResult; |
| |
| namespace offline_pages { |
| |
| namespace { |
| |
| // The delay used to schedule the first clear storage request for storage |
| // manager after the model is loaded. |
| const base::TimeDelta kStorageManagerStartingDelay = |
| base::TimeDelta::FromSeconds(20); |
| |
| int64_t GenerateOfflineId() { |
| return base::RandGenerator(std::numeric_limits<int64_t>::max()) + 1; |
| } |
| |
| // The maximum histogram size for the metrics that measure time between views of |
| // a given page. |
| const base::TimeDelta kMaxOpenedPageHistogramBucket = |
| base::TimeDelta::FromDays(90); |
| |
| SavePageResult ToSavePageResult(ArchiverResult archiver_result) { |
| SavePageResult result; |
| switch (archiver_result) { |
| case ArchiverResult::SUCCESSFULLY_CREATED: |
| result = SavePageResult::SUCCESS; |
| break; |
| case ArchiverResult::ERROR_DEVICE_FULL: |
| result = SavePageResult::DEVICE_FULL; |
| break; |
| case ArchiverResult::ERROR_CONTENT_UNAVAILABLE: |
| result = SavePageResult::CONTENT_UNAVAILABLE; |
| break; |
| case ArchiverResult::ERROR_ARCHIVE_CREATION_FAILED: |
| result = SavePageResult::ARCHIVE_CREATION_FAILED; |
| break; |
| case ArchiverResult::ERROR_CANCELED: |
| result = SavePageResult::CANCELLED; |
| break; |
| case ArchiverResult::ERROR_SECURITY_CERTIFICATE: |
| result = SavePageResult::SECURITY_CERTIFICATE_ERROR; |
| break; |
| case ArchiverResult::ERROR_ERROR_PAGE: |
| result = SavePageResult::ERROR_PAGE; |
| break; |
| case ArchiverResult::ERROR_INTERSTITIAL_PAGE: |
| result = SavePageResult::INTERSTITIAL_PAGE; |
| default: |
| NOTREACHED(); |
| result = SavePageResult::CONTENT_UNAVAILABLE; |
| } |
| return result; |
| } |
| |
| std::string AddHistogramSuffix(const ClientId& client_id, |
| const char* histogram_name) { |
| if (client_id.name_space.empty()) { |
| NOTREACHED(); |
| return histogram_name; |
| } |
| std::string adjusted_histogram_name(histogram_name); |
| adjusted_histogram_name += "."; |
| adjusted_histogram_name += client_id.name_space; |
| return adjusted_histogram_name; |
| } |
| |
| void ReportStorageHistogramsAfterSave( |
| const ArchiveManager::StorageStats& storage_stats) { |
| const int kMB = 1024 * 1024; |
| int free_disk_space_mb = |
| static_cast<int>(storage_stats.free_disk_space / kMB); |
| UMA_HISTOGRAM_CUSTOM_COUNTS("OfflinePages.SavePage.FreeSpaceMB", |
| free_disk_space_mb, 1, 500000, 50); |
| |
| int total_page_size_mb = |
| static_cast<int>(storage_stats.total_archives_size / kMB); |
| UMA_HISTOGRAM_COUNTS_10000("OfflinePages.TotalPageSize", total_page_size_mb); |
| } |
| |
| void ReportStorageHistogramsAfterDelete( |
| const ArchiveManager::StorageStats& storage_stats) { |
| const int kMB = 1024 * 1024; |
| int free_disk_space_mb = |
| static_cast<int>(storage_stats.free_disk_space / kMB); |
| UMA_HISTOGRAM_CUSTOM_COUNTS("OfflinePages.DeletePage.FreeSpaceMB", |
| free_disk_space_mb, 1, 500000, 50); |
| |
| int total_page_size_mb = |
| static_cast<int>(storage_stats.total_archives_size / kMB); |
| UMA_HISTOGRAM_COUNTS_10000("OfflinePages.TotalPageSize", total_page_size_mb); |
| |
| if (storage_stats.free_disk_space > 0) { |
| int percentage_of_free = static_cast<int>( |
| 1.0 * storage_stats.total_archives_size / |
| (storage_stats.total_archives_size + storage_stats.free_disk_space) * |
| 100); |
| UMA_HISTOGRAM_PERCENTAGE( |
| "OfflinePages.DeletePage.TotalPageSizeAsPercentageOfFreeSpace", |
| percentage_of_free); |
| } |
| } |
| |
| void ReportSavePageResultHistogramAfterSave(const ClientId& client_id, |
| SavePageResult result) { |
| // The histogram below is an expansion of the UMA_HISTOGRAM_ENUMERATION |
| // macro adapted to allow for a dynamically suffixed histogram name. |
| // Note: The factory creates and owns the histogram. |
| base::HistogramBase* histogram = base::LinearHistogram::FactoryGet( |
| AddHistogramSuffix(client_id, "OfflinePages.SavePageResult"), 1, |
| static_cast<int>(SavePageResult::RESULT_COUNT), |
| static_cast<int>(SavePageResult::RESULT_COUNT) + 1, |
| base::HistogramBase::kUmaTargetedHistogramFlag); |
| histogram->Add(static_cast<int>(result)); |
| } |
| |
| // Goes through the list of offline pages, compiling the following two metrics: |
| // - a count of the pages with the same URL |
| // - The difference between the |created_before| time and the creation time of |
| // the page with the closest creation time before |created_before|. |
| // Returns true if there was a page that was saved before |created_before| with |
| // a matching URL. |
| bool GetMatchingURLCountAndMostRecentCreationTime( |
| const std::map<int64_t, OfflinePageItem>& offline_pages, |
| std::string name_space, |
| const GURL& url, |
| base::Time created_before, |
| int* matching_url_count, |
| base::TimeDelta* most_recent_creation_time) { |
| int count = 0; |
| |
| // Create a time that is very old, so that any valid time will be newer than |
| // it. |
| base::Time latest_time; |
| bool matching_page = false; |
| |
| for (auto& id_page_pair : offline_pages) { |
| if (id_page_pair.second.client_id.name_space == name_space && |
| url == id_page_pair.second.url) { |
| count++; |
| base::Time page_creation_time = id_page_pair.second.creation_time; |
| if (page_creation_time < created_before && |
| page_creation_time > latest_time) { |
| latest_time = page_creation_time; |
| matching_page = true; |
| } |
| } |
| } |
| |
| if (matching_url_count != nullptr) |
| *matching_url_count = count; |
| if (most_recent_creation_time != nullptr && latest_time != base::Time()) |
| *most_recent_creation_time = created_before - latest_time; |
| |
| return matching_page; |
| } |
| |
| void ReportPageHistogramAfterSave( |
| ClientPolicyController* policy_controller_, |
| const std::map<int64_t, OfflinePageItem>& offline_pages, |
| const OfflinePageItem& offline_page, |
| const base::Time& save_time) { |
| DCHECK(policy_controller_); |
| // The histogram below is an expansion of the UMA_HISTOGRAM_TIMES |
| // macro adapted to allow for a dynamically suffixed histogram name. |
| // Note: The factory creates and owns the histogram. |
| base::HistogramBase* histogram = base::Histogram::FactoryTimeGet( |
| AddHistogramSuffix(offline_page.client_id, "OfflinePages.SavePageTime"), |
| base::TimeDelta::FromMilliseconds(1), base::TimeDelta::FromSeconds(10), |
| 50, base::HistogramBase::kUmaTargetedHistogramFlag); |
| histogram->AddTime(save_time - offline_page.creation_time); |
| |
| // The histogram below is an expansion of the UMA_HISTOGRAM_CUSTOM_COUNTS |
| // macro adapted to allow for a dynamically suffixed histogram name. |
| // Note: The factory creates and owns the histogram. |
| // Reported as Kb between 1Kb and 10Mb. |
| histogram = base::Histogram::FactoryGet( |
| AddHistogramSuffix(offline_page.client_id, "OfflinePages.PageSize"), 1, |
| 10000, 50, base::HistogramBase::kUmaTargetedHistogramFlag); |
| histogram->Add(offline_page.file_size / 1024); |
| |
| if (policy_controller_->IsSupportedByDownload( |
| offline_page.client_id.name_space)) { |
| int matching_url_count; |
| base::TimeDelta time_since_most_recent_duplicate; |
| if (GetMatchingURLCountAndMostRecentCreationTime( |
| offline_pages, offline_page.client_id.name_space, offline_page.url, |
| offline_page.creation_time, &matching_url_count, |
| &time_since_most_recent_duplicate)) { |
| // Using CUSTOM_COUNTS instead of time-oriented histogram to record |
| // samples in seconds rather than milliseconds. |
| UMA_HISTOGRAM_CUSTOM_COUNTS( |
| "OfflinePages.DownloadSavedPageTimeSinceDuplicateSaved", |
| time_since_most_recent_duplicate.InSeconds(), |
| base::TimeDelta::FromSeconds(1).InSeconds(), |
| base::TimeDelta::FromDays(7).InSeconds(), 50); |
| } |
| UMA_HISTOGRAM_CUSTOM_COUNTS("OfflinePages.DownloadSavedPageDuplicateCount", |
| matching_url_count, 1, 20, 10); |
| } |
| } |
| |
| void ReportPageHistogramsAfterDelete( |
| const std::map<int64_t, OfflinePageItem>& offline_pages, |
| const std::vector<OfflinePageItem>& deleted_pages, |
| const base::Time& delete_time) { |
| const int max_minutes = base::TimeDelta::FromDays(365).InMinutes(); |
| int64_t total_size = 0; |
| |
| for (const auto& page : deleted_pages) { |
| total_size += page.file_size; |
| ClientId client_id = page.client_id; |
| |
| if (client_id.name_space == kDownloadNamespace) { |
| int remaining_pages_with_url; |
| GetMatchingURLCountAndMostRecentCreationTime( |
| offline_pages, page.client_id.name_space, page.url, base::Time::Max(), |
| &remaining_pages_with_url, nullptr); |
| UMA_HISTOGRAM_CUSTOM_COUNTS( |
| "OfflinePages.DownloadDeletedPageDuplicateCount", |
| remaining_pages_with_url, 1, 20, 10); |
| } |
| |
| // The histograms below are an expansion of the UMA_HISTOGRAM_CUSTOM_COUNTS |
| // macro adapted to allow for a dynamically suffixed histogram name. |
| // Note: The factory creates and owns the histogram. |
| base::HistogramBase* histogram = base::Histogram::FactoryGet( |
| AddHistogramSuffix(client_id, "OfflinePages.PageLifetime"), 1, |
| max_minutes, 100, base::HistogramBase::kUmaTargetedHistogramFlag); |
| histogram->Add((delete_time - page.creation_time).InMinutes()); |
| |
| histogram = base::Histogram::FactoryGet( |
| AddHistogramSuffix(client_id, |
| "OfflinePages.DeletePage.TimeSinceLastOpen"), |
| 1, max_minutes, 100, base::HistogramBase::kUmaTargetedHistogramFlag); |
| histogram->Add((delete_time - page.last_access_time).InMinutes()); |
| |
| histogram = base::Histogram::FactoryGet( |
| AddHistogramSuffix(client_id, |
| "OfflinePages.DeletePage.LastOpenToCreated"), |
| 1, max_minutes, 100, base::HistogramBase::kUmaTargetedHistogramFlag); |
| histogram->Add((page.last_access_time - page.creation_time).InMinutes()); |
| |
| // Reported as Kb between 1Kb and 10Mb. |
| histogram = base::Histogram::FactoryGet( |
| AddHistogramSuffix(client_id, "OfflinePages.DeletePage.PageSize"), 1, |
| 10000, 50, base::HistogramBase::kUmaTargetedHistogramFlag); |
| histogram->Add(page.file_size / 1024); |
| |
| histogram = base::Histogram::FactoryGet( |
| AddHistogramSuffix(client_id, "OfflinePages.DeletePage.AccessCount"), 1, |
| 1000000, 50, base::HistogramBase::kUmaTargetedHistogramFlag); |
| histogram->Add(page.access_count); |
| } |
| |
| if (deleted_pages.size() > 1) { |
| UMA_HISTOGRAM_COUNTS("OfflinePages.BatchDelete.Count", |
| static_cast<int32_t>(deleted_pages.size())); |
| UMA_HISTOGRAM_MEMORY_KB("OfflinePages.BatchDelete.TotalPageSize", |
| total_size / 1024); |
| } |
| } |
| |
| void ReportPageHistogramsAfterAccess(const OfflinePageItem& offline_page_item, |
| const base::Time& access_time) { |
| // The histogram below is an expansion of the UMA_HISTOGRAM_CUSTOM_COUNTS |
| // macro adapted to allow for a dynamically suffixed histogram name. |
| // Note: The factory creates and owns the histogram. |
| base::HistogramBase* histogram = base::Histogram::FactoryGet( |
| AddHistogramSuffix(offline_page_item.client_id, |
| offline_page_item.access_count == 0 |
| ? "OfflinePages.FirstOpenSinceCreated" |
| : "OfflinePages.OpenSinceLastOpen"), |
| 1, kMaxOpenedPageHistogramBucket.InMinutes(), 50, |
| base::HistogramBase::kUmaTargetedHistogramFlag); |
| histogram->Add( |
| (access_time - offline_page_item.last_access_time).InMinutes()); |
| } |
| |
| } // namespace |
| |
| // protected |
| OfflinePageModelImpl::OfflinePageModelImpl() |
| : OfflinePageModel(), is_loaded_(false), weak_ptr_factory_(this) {} |
| |
| OfflinePageModelImpl::OfflinePageModelImpl( |
| std::unique_ptr<OfflinePageMetadataStore> store, |
| const base::FilePath& archives_dir, |
| const scoped_refptr<base::SequencedTaskRunner>& task_runner) |
| : store_(std::move(store)), |
| archives_dir_(archives_dir), |
| is_loaded_(false), |
| policy_controller_(new ClientPolicyController()), |
| archive_manager_(new ArchiveManager(archives_dir, task_runner)), |
| testing_clock_(nullptr), |
| weak_ptr_factory_(this) { |
| archive_manager_->EnsureArchivesDirCreated( |
| base::Bind(&OfflinePageModelImpl::OnEnsureArchivesDirCreatedDone, |
| weak_ptr_factory_.GetWeakPtr(), base::TimeTicks::Now())); |
| } |
| |
| OfflinePageModelImpl::~OfflinePageModelImpl() {} |
| |
| void OfflinePageModelImpl::AddObserver(Observer* observer) { |
| observers_.AddObserver(observer); |
| } |
| |
| void OfflinePageModelImpl::RemoveObserver(Observer* observer) { |
| observers_.RemoveObserver(observer); |
| } |
| |
| void OfflinePageModelImpl::SavePage( |
| const SavePageParams& save_page_params, |
| std::unique_ptr<OfflinePageArchiver> archiver, |
| const SavePageCallback& callback) { |
| DCHECK(is_loaded_); |
| |
| // Skip saving the page that is not intended to be saved, like local file |
| // page. |
| if (!OfflinePageModel::CanSaveURL(save_page_params.url)) { |
| InformSavePageDone(callback, SavePageResult::SKIPPED, |
| save_page_params.client_id, kInvalidOfflineId); |
| return; |
| } |
| |
| // The web contents is not available if archiver is not created and passed. |
| if (!archiver.get()) { |
| InformSavePageDone(callback, SavePageResult::CONTENT_UNAVAILABLE, |
| save_page_params.client_id, kInvalidOfflineId); |
| return; |
| } |
| |
| // If we already have an offline id, use it. If not, generate one. |
| int64_t offline_id = save_page_params.proposed_offline_id; |
| if (offline_id == kInvalidOfflineId) |
| offline_id = GenerateOfflineId(); |
| |
| OfflinePageArchiver::CreateArchiveParams create_archive_params; |
| // If the page is being saved in the background, we should try to remove the |
| // popup overlay that obstructs viewing the normal content. |
| create_archive_params.remove_popup_overlay = save_page_params.is_background; |
| archiver->CreateArchive( |
| archives_dir_, create_archive_params, |
| base::Bind(&OfflinePageModelImpl::OnCreateArchiveDone, |
| weak_ptr_factory_.GetWeakPtr(), save_page_params, offline_id, |
| GetCurrentTime(), callback)); |
| pending_archivers_.push_back(std::move(archiver)); |
| } |
| |
| void OfflinePageModelImpl::MarkPageAccessed(int64_t offline_id) { |
| RunWhenLoaded(base::Bind(&OfflinePageModelImpl::MarkPageAccessedWhenLoadDone, |
| weak_ptr_factory_.GetWeakPtr(), offline_id)); |
| } |
| |
| void OfflinePageModelImpl::MarkPageAccessedWhenLoadDone(int64_t offline_id) { |
| DCHECK(is_loaded_); |
| |
| auto iter = offline_pages_.find(offline_id); |
| if (iter == offline_pages_.end()) |
| return; |
| |
| // Make a copy of the cached item and update it. The cached item should only |
| // be updated upon the successful store operation. |
| OfflinePageItem offline_page_item = iter->second; |
| |
| ReportPageHistogramsAfterAccess(offline_page_item, GetCurrentTime()); |
| |
| offline_page_item.last_access_time = GetCurrentTime(); |
| offline_page_item.access_count++; |
| |
| std::vector<OfflinePageItem> items = {offline_page_item}; |
| store_->UpdateOfflinePages( |
| items, base::Bind(&OfflinePageModelImpl::OnMarkPageAccesseDone, |
| weak_ptr_factory_.GetWeakPtr(), offline_page_item)); |
| } |
| |
| void OfflinePageModelImpl::DeletePagesByOfflineId( |
| const std::vector<int64_t>& offline_ids, |
| const DeletePageCallback& callback) { |
| RunWhenLoaded(base::Bind(&OfflinePageModelImpl::DoDeletePagesByOfflineId, |
| weak_ptr_factory_.GetWeakPtr(), offline_ids, |
| callback)); |
| } |
| |
| void OfflinePageModelImpl::DoDeletePagesByOfflineId( |
| const std::vector<int64_t>& offline_ids, |
| const DeletePageCallback& callback) { |
| DCHECK(is_loaded_); |
| |
| std::vector<base::FilePath> paths_to_delete; |
| for (const auto& offline_id : offline_ids) { |
| auto iter = offline_pages_.find(offline_id); |
| if (iter != offline_pages_.end()) { |
| paths_to_delete.push_back(iter->second.file_path); |
| } |
| } |
| |
| // If there're no pages to delete, return early. |
| if (paths_to_delete.empty()) { |
| InformDeletePageDone(callback, DeletePageResult::SUCCESS); |
| return; |
| } |
| |
| archive_manager_->DeleteMultipleArchives( |
| paths_to_delete, |
| base::Bind(&OfflinePageModelImpl::OnDeleteArchiveFilesDone, |
| weak_ptr_factory_.GetWeakPtr(), offline_ids, callback)); |
| } |
| |
| void OfflinePageModelImpl::DeletePagesByClientIds( |
| const std::vector<ClientId>& client_ids, |
| const DeletePageCallback& callback) { |
| OfflinePageModelQueryBuilder builder; |
| builder.SetClientIds(OfflinePageModelQuery::Requirement::INCLUDE_MATCHING, |
| client_ids); |
| auto delete_pages = base::Bind(&OfflinePageModelImpl::DeletePages, |
| weak_ptr_factory_.GetWeakPtr(), callback); |
| RunWhenLoaded(base::Bind( |
| &OfflinePageModelImpl::GetPagesMatchingQueryWhenLoadDone, |
| weak_ptr_factory_.GetWeakPtr(), |
| base::Passed(builder.Build(GetPolicyController())), delete_pages)); |
| } |
| |
| void OfflinePageModelImpl::DeletePages( |
| const DeletePageCallback& callback, |
| const MultipleOfflinePageItemResult& pages) { |
| DCHECK(is_loaded_); |
| |
| std::vector<int64_t> offline_ids; |
| for (auto& page : pages) |
| offline_ids.emplace_back(page.offline_id); |
| |
| DoDeletePagesByOfflineId(offline_ids, callback); |
| } |
| |
| void OfflinePageModelImpl::GetPagesMatchingQuery( |
| std::unique_ptr<OfflinePageModelQuery> query, |
| const MultipleOfflinePageItemCallback& callback) { |
| RunWhenLoaded(base::Bind( |
| &OfflinePageModelImpl::GetPagesMatchingQueryWhenLoadDone, |
| weak_ptr_factory_.GetWeakPtr(), base::Passed(&query), callback)); |
| } |
| |
| void OfflinePageModelImpl::GetPagesMatchingQueryWhenLoadDone( |
| std::unique_ptr<OfflinePageModelQuery> query, |
| const MultipleOfflinePageItemCallback& callback) { |
| DCHECK(query); |
| DCHECK(is_loaded_); |
| |
| MultipleOfflinePageItemResult offline_pages_result; |
| |
| for (const auto& id_page_pair : offline_pages_) { |
| if (query->Matches(id_page_pair.second)) |
| offline_pages_result.emplace_back(id_page_pair.second); |
| } |
| |
| callback.Run(offline_pages_result); |
| } |
| |
| void OfflinePageModelImpl::GetPagesByClientIds( |
| const std::vector<ClientId>& client_ids, |
| const MultipleOfflinePageItemCallback& callback) { |
| OfflinePageModelQueryBuilder builder; |
| builder.SetClientIds(OfflinePageModelQuery::Requirement::INCLUDE_MATCHING, |
| client_ids); |
| RunWhenLoaded( |
| base::Bind(&OfflinePageModelImpl::GetPagesMatchingQueryWhenLoadDone, |
| weak_ptr_factory_.GetWeakPtr(), |
| base::Passed(builder.Build(GetPolicyController())), callback)); |
| } |
| |
| void OfflinePageModelImpl::DeleteCachedPagesByURLPredicate( |
| const UrlPredicate& predicate, |
| const DeletePageCallback& callback) { |
| RunWhenLoaded( |
| base::Bind(&OfflinePageModelImpl::DoDeleteCachedPagesByURLPredicate, |
| weak_ptr_factory_.GetWeakPtr(), predicate, callback)); |
| } |
| |
| void OfflinePageModelImpl::DoDeleteCachedPagesByURLPredicate( |
| const UrlPredicate& predicate, |
| const DeletePageCallback& callback) { |
| DCHECK(is_loaded_); |
| |
| std::vector<int64_t> offline_ids; |
| for (const auto& id_page_pair : offline_pages_) { |
| if (IsRemovedOnCacheReset(id_page_pair.second) && |
| predicate.Run(id_page_pair.second.url)) { |
| offline_ids.push_back(id_page_pair.first); |
| } |
| } |
| DoDeletePagesByOfflineId(offline_ids, callback); |
| } |
| |
| void OfflinePageModelImpl::CheckPagesExistOffline( |
| const std::set<GURL>& urls, |
| const CheckPagesExistOfflineCallback& callback) { |
| OfflinePageModelQueryBuilder builder; |
| builder |
| .SetUrls(OfflinePageModelQuery::Requirement::INCLUDE_MATCHING, |
| std::vector<GURL>(urls.begin(), urls.end())) |
| .RequireRestrictedToOriginalTab( |
| OfflinePageModelQueryBuilder::Requirement::EXCLUDE_MATCHING); |
| auto pages_to_urls = base::Bind( |
| [](const CheckPagesExistOfflineCallback& callback, |
| const MultipleOfflinePageItemResult& pages) { |
| CheckPagesExistOfflineResult result; |
| for (auto& page : pages) |
| result.insert(page.url); |
| callback.Run(result); |
| }, |
| callback); |
| RunWhenLoaded(base::Bind( |
| &OfflinePageModelImpl::GetPagesMatchingQueryWhenLoadDone, |
| weak_ptr_factory_.GetWeakPtr(), |
| base::Passed(builder.Build(GetPolicyController())), pages_to_urls)); |
| } |
| |
| void OfflinePageModelImpl::GetAllPages( |
| const MultipleOfflinePageItemCallback& callback) { |
| OfflinePageModelQueryBuilder builder; |
| RunWhenLoaded( |
| base::Bind(&OfflinePageModelImpl::GetPagesMatchingQueryWhenLoadDone, |
| weak_ptr_factory_.GetWeakPtr(), |
| base::Passed(builder.Build(GetPolicyController())), callback)); |
| } |
| |
| void OfflinePageModelImpl::GetOfflineIdsForClientId( |
| const ClientId& client_id, |
| const MultipleOfflineIdCallback& callback) { |
| RunWhenLoaded( |
| base::Bind(&OfflinePageModelImpl::GetOfflineIdsForClientIdWhenLoadDone, |
| weak_ptr_factory_.GetWeakPtr(), client_id, callback)); |
| } |
| |
| void OfflinePageModelImpl::GetOfflineIdsForClientIdWhenLoadDone( |
| const ClientId& client_id, |
| const MultipleOfflineIdCallback& callback) const { |
| DCHECK(is_loaded_); |
| callback.Run(MaybeGetOfflineIdsForClientId(client_id)); |
| } |
| |
| const std::vector<int64_t> OfflinePageModelImpl::MaybeGetOfflineIdsForClientId( |
| const ClientId& client_id) const { |
| DCHECK(is_loaded_); |
| std::vector<int64_t> results; |
| |
| // We want only all pages, including those marked for deletion. |
| for (const auto& id_page_pair : offline_pages_) { |
| if (id_page_pair.second.client_id == client_id) |
| results.push_back(id_page_pair.second.offline_id); |
| } |
| return results; |
| } |
| |
| void OfflinePageModelImpl::GetPageByOfflineId( |
| int64_t offline_id, |
| const SingleOfflinePageItemCallback& callback) { |
| std::vector<int64_t> query_ids; |
| query_ids.emplace_back(offline_id); |
| |
| OfflinePageModelQueryBuilder builder; |
| builder.SetOfflinePageIds( |
| OfflinePageModelQuery::Requirement::INCLUDE_MATCHING, query_ids); |
| |
| auto multiple_callback = base::Bind( |
| [](const SingleOfflinePageItemCallback& callback, |
| const MultipleOfflinePageItemResult& result) { |
| DCHECK_LE(result.size(), 1U); |
| if (result.empty()) { |
| callback.Run(nullptr); |
| } else { |
| callback.Run(&result[0]); |
| } |
| }, |
| callback); |
| |
| RunWhenLoaded(base::Bind( |
| &OfflinePageModelImpl::GetPagesMatchingQueryWhenLoadDone, |
| weak_ptr_factory_.GetWeakPtr(), |
| base::Passed(builder.Build(GetPolicyController())), multiple_callback)); |
| } |
| |
| void OfflinePageModelImpl::GetPagesByURL( |
| const GURL& url, |
| URLSearchMode url_search_mode, |
| const MultipleOfflinePageItemCallback& callback) { |
| RunWhenLoaded( |
| base::Bind(&OfflinePageModelImpl::GetPagesByURLWhenLoadDone, |
| weak_ptr_factory_.GetWeakPtr(), url, |
| url_search_mode, callback)); |
| } |
| |
| void OfflinePageModelImpl::GetPagesByURLWhenLoadDone( |
| const GURL& url, |
| URLSearchMode url_search_mode, |
| const MultipleOfflinePageItemCallback& callback) const { |
| DCHECK(is_loaded_); |
| std::vector<OfflinePageItem> result; |
| |
| GURL::Replacements remove_params; |
| remove_params.ClearRef(); |
| |
| GURL url_without_fragment = |
| url.ReplaceComponents(remove_params); |
| |
| for (const auto& id_page_pair : offline_pages_) { |
| // First, search by last committed URL with fragment stripped. |
| if (url_without_fragment == |
| id_page_pair.second.url.ReplaceComponents(remove_params)) { |
| result.push_back(id_page_pair.second); |
| continue; |
| } |
| // Then, search by original request URL if |url_search_mode| wants it. |
| // Note that we want to do the exact match with fragment included. This is |
| // because original URL is used for redirect purpose and it is always safer |
| // to support the exact redirect. |
| if (url_search_mode == URLSearchMode::SEARCH_BY_ALL_URLS && |
| url == id_page_pair.second.original_url) { |
| result.push_back(id_page_pair.second); |
| } |
| } |
| |
| callback.Run(result); |
| } |
| |
| void OfflinePageModelImpl::CheckMetadataConsistency() { |
| archive_manager_->GetAllArchives( |
| base::Bind(&OfflinePageModelImpl::CheckMetadataConsistencyForArchivePaths, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| ClientPolicyController* OfflinePageModelImpl::GetPolicyController() { |
| return policy_controller_.get(); |
| } |
| |
| OfflinePageMetadataStore* OfflinePageModelImpl::GetStoreForTesting() { |
| return store_.get(); |
| } |
| |
| OfflinePageStorageManager* OfflinePageModelImpl::GetStorageManager() { |
| return storage_manager_.get(); |
| } |
| |
| bool OfflinePageModelImpl::is_loaded() const { |
| return is_loaded_; |
| } |
| |
| OfflineEventLogger* OfflinePageModelImpl::GetLogger() { |
| return &offline_event_logger_; |
| } |
| |
| void OfflinePageModelImpl::OnCreateArchiveDone( |
| const SavePageParams& save_page_params, |
| int64_t offline_id, |
| const base::Time& start_time, |
| const SavePageCallback& callback, |
| OfflinePageArchiver* archiver, |
| ArchiverResult archiver_result, |
| const GURL& url, |
| const base::FilePath& file_path, |
| const base::string16& title, |
| int64_t file_size) { |
| if (save_page_params.url != url) { |
| DVLOG(1) << "Saved URL does not match requested URL."; |
| InformSavePageDone(callback, SavePageResult::ARCHIVE_CREATION_FAILED, |
| save_page_params.client_id, offline_id); |
| DeletePendingArchiver(archiver); |
| return; |
| } |
| |
| if (archiver_result != ArchiverResult::SUCCESSFULLY_CREATED) { |
| SavePageResult result = ToSavePageResult(archiver_result); |
| InformSavePageDone( |
| callback, result, save_page_params.client_id, offline_id); |
| DeletePendingArchiver(archiver); |
| return; |
| } |
| OfflinePageItem offline_page_item(url, offline_id, save_page_params.client_id, |
| file_path, file_size, start_time); |
| offline_page_item.title = title; |
| offline_page_item.original_url = save_page_params.original_url; |
| store_->AddOfflinePage(offline_page_item, |
| base::Bind(&OfflinePageModelImpl::OnAddOfflinePageDone, |
| weak_ptr_factory_.GetWeakPtr(), archiver, |
| file_path, callback, offline_page_item)); |
| } |
| |
| void OfflinePageModelImpl::OnAddOfflinePageDone( |
| OfflinePageArchiver* archiver, |
| const base::FilePath& file_path, |
| const SavePageCallback& callback, |
| const OfflinePageItem& offline_page, |
| ItemActionStatus status) { |
| SavePageResult result; |
| |
| if (status == ItemActionStatus::SUCCESS) { |
| offline_pages_[offline_page.offline_id] = offline_page; |
| result = SavePageResult::SUCCESS; |
| ReportPageHistogramAfterSave(policy_controller_.get(), offline_pages_, |
| offline_page, GetCurrentTime()); |
| offline_event_logger_.RecordPageSaved(offline_page.client_id.name_space, |
| offline_page.url.spec(), |
| offline_page.offline_id); |
| } else if (status == ItemActionStatus::ALREADY_EXISTS) { |
| // Remove the orphaned archive. No callback necessary. |
| archive_manager_->DeleteArchive(file_path, base::Bind([](bool) {})); |
| result = SavePageResult::ALREADY_EXISTS; |
| } else { |
| result = SavePageResult::STORE_FAILURE; |
| } |
| InformSavePageDone(callback, result, offline_page.client_id, |
| offline_page.offline_id); |
| if (result == SavePageResult::SUCCESS) { |
| DeleteExistingPagesWithSameURL(offline_page); |
| } else { |
| PostClearStorageIfNeededTask(false /* delayed */); |
| } |
| |
| DeletePendingArchiver(archiver); |
| |
| // We don't want to notify observers if the add failed. |
| if (result != SavePageResult::SUCCESS) |
| return; |
| |
| for (Observer& observer : observers_) |
| observer.OfflinePageAdded(this, offline_page); |
| } |
| |
| void OfflinePageModelImpl::OnMarkPageAccesseDone( |
| const OfflinePageItem& offline_page_item, |
| std::unique_ptr<OfflinePagesUpdateResult> result) { |
| // Update the item in the cache only upon success. |
| if (result->updated_items.size() > 0) |
| offline_pages_[offline_page_item.offline_id] = offline_page_item; |
| |
| // No need to fire OfflinePageModelChanged event since updating access info |
| // should not have any impact to the UI. |
| } |
| |
| void OfflinePageModelImpl::OnEnsureArchivesDirCreatedDone( |
| const base::TimeTicks& start_time) { |
| UMA_HISTOGRAM_TIMES("OfflinePages.Model.ArchiveDirCreationTime", |
| base::TimeTicks::Now() - start_time); |
| |
| const int kResetAttemptsLeft = 1; |
| store_->Initialize(base::Bind(&OfflinePageModelImpl::OnStoreInitialized, |
| weak_ptr_factory_.GetWeakPtr(), start_time, |
| kResetAttemptsLeft)); |
| } |
| |
| void OfflinePageModelImpl::OnStoreInitialized(const base::TimeTicks& start_time, |
| int reset_attempts_left, |
| bool success) { |
| if (success) { |
| DCHECK_EQ(store_->state(), StoreState::LOADED); |
| store_->GetOfflinePages( |
| base::Bind(&OfflinePageModelImpl::OnInitialGetOfflinePagesDone, |
| weak_ptr_factory_.GetWeakPtr(), start_time)); |
| return; |
| } |
| |
| DCHECK_EQ(store_->state(), StoreState::FAILED_LOADING); |
| // If there are no more reset attempts left, stop here. |
| if (reset_attempts_left == 0) { |
| FinalizeModelLoad(); |
| return; |
| } |
| |
| // Otherwise reduce the remaining attempts counter and reset store. |
| store_->Reset(base::Bind(&OfflinePageModelImpl::OnStoreResetDone, |
| weak_ptr_factory_.GetWeakPtr(), start_time, |
| reset_attempts_left - 1)); |
| } |
| |
| void OfflinePageModelImpl::OnStoreResetDone(const base::TimeTicks& start_time, |
| int reset_attempts_left, |
| bool success) { |
| if (success) { |
| DCHECK_EQ(store_->state(), StoreState::NOT_LOADED); |
| store_->Initialize(base::Bind(&OfflinePageModelImpl::OnStoreInitialized, |
| weak_ptr_factory_.GetWeakPtr(), start_time, |
| reset_attempts_left)); |
| return; |
| } |
| |
| DCHECK_EQ(store_->state(), StoreState::FAILED_RESET); |
| FinalizeModelLoad(); |
| } |
| |
| void OfflinePageModelImpl::OnInitialGetOfflinePagesDone( |
| const base::TimeTicks& start_time, |
| const std::vector<OfflinePageItem>& offline_pages) { |
| DCHECK(!is_loaded_); |
| |
| UMA_HISTOGRAM_TIMES("OfflinePages.Model.ConstructionToLoadedEventTime", |
| base::TimeTicks::Now() - start_time); |
| |
| CacheLoadedData(offline_pages); |
| FinalizeModelLoad(); |
| |
| // Ensure necessary cleanup operations are started. |
| CheckMetadataConsistency(); |
| } |
| |
| void OfflinePageModelImpl::FinalizeModelLoad() { |
| is_loaded_ = true; |
| |
| // All actions below are meant to be taken regardless of successful load of |
| // the store. |
| |
| // Inform observers the load is done. |
| for (Observer& observer : observers_) |
| observer.OfflinePageModelLoaded(this); |
| |
| // Run all the delayed tasks. |
| for (const auto& delayed_task : delayed_tasks_) |
| delayed_task.Run(); |
| delayed_tasks_.clear(); |
| |
| // Clear storage. |
| PostClearStorageIfNeededTask(true /* delayed */); |
| } |
| |
| void OfflinePageModelImpl::InformSavePageDone(const SavePageCallback& callback, |
| SavePageResult result, |
| const ClientId& client_id, |
| int64_t offline_id) { |
| ReportSavePageResultHistogramAfterSave(client_id, result); |
| archive_manager_->GetStorageStats( |
| base::Bind(&ReportStorageHistogramsAfterSave)); |
| callback.Run(result, offline_id); |
| } |
| |
| void OfflinePageModelImpl::DeleteExistingPagesWithSameURL( |
| const OfflinePageItem& offline_page) { |
| // Remove existing pages generated by the same policy and with same url. |
| size_t pages_allowed = |
| policy_controller_->GetPolicy(offline_page.client_id.name_space) |
| .pages_allowed_per_url; |
| if (pages_allowed == kUnlimitedPages) |
| return; |
| GetPagesByURL( |
| offline_page.url, |
| URLSearchMode::SEARCH_BY_FINAL_URL_ONLY, |
| base::Bind(&OfflinePageModelImpl::OnPagesFoundWithSameURL, |
| weak_ptr_factory_.GetWeakPtr(), offline_page, pages_allowed)); |
| } |
| |
| void OfflinePageModelImpl::OnPagesFoundWithSameURL( |
| const OfflinePageItem& offline_page, |
| size_t pages_allowed, |
| const MultipleOfflinePageItemResult& items) { |
| std::vector<OfflinePageItem> pages_to_delete; |
| for (const auto& item : items) { |
| if (item.offline_id != offline_page.offline_id && |
| item.client_id.name_space == offline_page.client_id.name_space) { |
| pages_to_delete.push_back(item); |
| } |
| } |
| // Only keep |pages_allowed|-1 of most fresh pages and delete others, by |
| // sorting pages with the oldest ones first and resize the vector. |
| if (pages_to_delete.size() >= pages_allowed) { |
| sort(pages_to_delete.begin(), pages_to_delete.end(), |
| [](const OfflinePageItem& a, const OfflinePageItem& b) -> bool { |
| return a.last_access_time < b.last_access_time; |
| }); |
| pages_to_delete.resize(pages_to_delete.size() - pages_allowed + 1); |
| } |
| std::vector<int64_t> page_ids_to_delete; |
| for (const auto& item : pages_to_delete) |
| page_ids_to_delete.push_back(item.offline_id); |
| DeletePagesByOfflineId( |
| page_ids_to_delete, |
| base::Bind(&OfflinePageModelImpl::OnDeleteOldPagesWithSameURL, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void OfflinePageModelImpl::OnDeleteOldPagesWithSameURL( |
| DeletePageResult result) { |
| PostClearStorageIfNeededTask(false /* delayed */); |
| } |
| |
| void OfflinePageModelImpl::DeletePendingArchiver( |
| OfflinePageArchiver* archiver) { |
| pending_archivers_.erase( |
| std::find_if(pending_archivers_.begin(), pending_archivers_.end(), |
| [archiver](const std::unique_ptr<OfflinePageArchiver>& a) { |
| return a.get() == archiver; |
| })); |
| } |
| |
| void OfflinePageModelImpl::OnDeleteArchiveFilesDone( |
| const std::vector<int64_t>& offline_ids, |
| const DeletePageCallback& callback, |
| bool success) { |
| if (!success) { |
| InformDeletePageDone(callback, DeletePageResult::DEVICE_FAILURE); |
| return; |
| } |
| |
| store_->RemoveOfflinePages( |
| offline_ids, base::Bind(&OfflinePageModelImpl::OnRemoveOfflinePagesDone, |
| weak_ptr_factory_.GetWeakPtr(), callback)); |
| } |
| |
| void OfflinePageModelImpl::OnRemoveOfflinePagesDone( |
| const DeletePageCallback& callback, |
| std::unique_ptr<OfflinePagesUpdateResult> result) { |
| ReportPageHistogramsAfterDelete(offline_pages_, result->updated_items, |
| GetCurrentTime()); |
| |
| // This part of the loop is explicitly broken out, as it should be gone in |
| // fully asynchronous code. |
| for (const auto& page : result->updated_items) { |
| int64_t offline_id = page.offline_id; |
| offline_event_logger_.RecordPageDeleted(offline_id); |
| auto iter = offline_pages_.find(offline_id); |
| if (iter == offline_pages_.end()) |
| continue; |
| offline_pages_.erase(iter); |
| } |
| |
| for (const auto& page : result->updated_items) { |
| for (Observer& observer : observers_) |
| observer.OfflinePageDeleted(page.offline_id, page.client_id); |
| } |
| |
| DeletePageResult delete_result; |
| if (result->store_state == StoreState::LOADED) |
| delete_result = DeletePageResult::SUCCESS; |
| else |
| delete_result = DeletePageResult::STORE_FAILURE; |
| |
| InformDeletePageDone(callback, delete_result); |
| } |
| |
| void OfflinePageModelImpl::InformDeletePageDone( |
| const DeletePageCallback& callback, |
| DeletePageResult result) { |
| UMA_HISTOGRAM_ENUMERATION("OfflinePages.DeletePageResult", |
| static_cast<int>(result), |
| static_cast<int>(DeletePageResult::RESULT_COUNT)); |
| archive_manager_->GetStorageStats( |
| base::Bind(&ReportStorageHistogramsAfterDelete)); |
| if (!callback.is_null()) |
| callback.Run(result); |
| } |
| |
| void OfflinePageModelImpl::CheckMetadataConsistencyForArchivePaths( |
| const std::set<base::FilePath>& archive_paths) { |
| DeletePagesMissingArchiveFile(archive_paths); |
| DeleteOrphanedArchives(archive_paths); |
| } |
| |
| void OfflinePageModelImpl::DeletePagesMissingArchiveFile( |
| const std::set<base::FilePath>& archive_paths) { |
| std::vector<int64_t> ids_of_pages_missing_archive_file; |
| for (const auto& id_page_pair : offline_pages_) { |
| if (archive_paths.count(id_page_pair.second.file_path) == 0UL) |
| ids_of_pages_missing_archive_file.push_back(id_page_pair.first); |
| } |
| |
| if (ids_of_pages_missing_archive_file.empty()) |
| return; |
| |
| DeletePagesByOfflineId( |
| ids_of_pages_missing_archive_file, |
| base::Bind(&OfflinePageModelImpl::OnDeletePagesMissingArchiveFileDone, |
| weak_ptr_factory_.GetWeakPtr(), |
| ids_of_pages_missing_archive_file)); |
| } |
| |
| void OfflinePageModelImpl::OnDeletePagesMissingArchiveFileDone( |
| const std::vector<int64_t>& offline_ids, |
| DeletePageResult result) { |
| UMA_HISTOGRAM_COUNTS("OfflinePages.Consistency.PagesMissingArchiveFileCount", |
| static_cast<int32_t>(offline_ids.size())); |
| UMA_HISTOGRAM_ENUMERATION( |
| "OfflinePages.Consistency.DeletePagesMissingArchiveFileResult", |
| static_cast<int>(result), |
| static_cast<int>(DeletePageResult::RESULT_COUNT)); |
| } |
| |
| void OfflinePageModelImpl::DeleteOrphanedArchives( |
| const std::set<base::FilePath>& archive_paths) { |
| // Archives are considered orphaned unless they are pointed to by some pages. |
| std::set<base::FilePath> orphaned_archive_set(archive_paths); |
| for (const auto& id_page_pair : offline_pages_) |
| orphaned_archive_set.erase(id_page_pair.second.file_path); |
| |
| if (orphaned_archive_set.empty()) |
| return; |
| |
| std::vector<base::FilePath> orphaned_archives(orphaned_archive_set.begin(), |
| orphaned_archive_set.end()); |
| archive_manager_->DeleteMultipleArchives( |
| orphaned_archives, |
| base::Bind(&OfflinePageModelImpl::OnDeleteOrphanedArchivesDone, |
| weak_ptr_factory_.GetWeakPtr(), orphaned_archives)); |
| } |
| |
| void OfflinePageModelImpl::OnDeleteOrphanedArchivesDone( |
| const std::vector<base::FilePath>& archives, |
| bool success) { |
| UMA_HISTOGRAM_COUNTS("OfflinePages.Consistency.OrphanedArchivesCount", |
| static_cast<int32_t>(archives.size())); |
| UMA_HISTOGRAM_BOOLEAN("OfflinePages.Consistency.DeleteOrphanedArchivesResult", |
| success); |
| } |
| |
| void OfflinePageModelImpl::CacheLoadedData( |
| const std::vector<OfflinePageItem>& offline_pages) { |
| offline_pages_.clear(); |
| for (const auto& offline_page : offline_pages) |
| offline_pages_[offline_page.offline_id] = offline_page; |
| } |
| |
| void OfflinePageModelImpl::ClearStorageIfNeeded( |
| const ClearStorageCallback& callback) { |
| // Create Storage Manager if necessary. |
| if (!storage_manager_) { |
| storage_manager_.reset(new OfflinePageStorageManager( |
| this, GetPolicyController(), archive_manager_.get())); |
| } |
| storage_manager_->ClearPagesIfNeeded(callback); |
| } |
| |
| void OfflinePageModelImpl::OnStorageCleared(size_t deleted_page_count, |
| ClearStorageResult result) { |
| UMA_HISTOGRAM_ENUMERATION("OfflinePages.ClearStorageResult", |
| static_cast<int>(result), |
| static_cast<int>(ClearStorageResult::RESULT_COUNT)); |
| if (deleted_page_count > 0) { |
| UMA_HISTOGRAM_COUNTS("OfflinePages.ClearStorageBatchSize", |
| static_cast<int32_t>(deleted_page_count)); |
| } |
| } |
| |
| void OfflinePageModelImpl::PostClearStorageIfNeededTask(bool delayed) { |
| base::TimeDelta delay = |
| delayed ? kStorageManagerStartingDelay : base::TimeDelta(); |
| base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( |
| FROM_HERE, base::Bind(&OfflinePageModelImpl::ClearStorageIfNeeded, |
| weak_ptr_factory_.GetWeakPtr(), |
| base::Bind(&OfflinePageModelImpl::OnStorageCleared, |
| weak_ptr_factory_.GetWeakPtr())), |
| delay); |
| } |
| |
| bool OfflinePageModelImpl::IsRemovedOnCacheReset( |
| const OfflinePageItem& offline_page) const { |
| return policy_controller_->IsRemovedOnCacheReset( |
| offline_page.client_id.name_space); |
| } |
| |
| void OfflinePageModelImpl::RunWhenLoaded(const base::Closure& task) { |
| if (!is_loaded_) { |
| delayed_tasks_.push_back(task); |
| return; |
| } |
| |
| base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, task); |
| } |
| |
| base::Time OfflinePageModelImpl::GetCurrentTime() const { |
| return testing_clock_ ? testing_clock_->Now() : base::Time::Now(); |
| } |
| |
| } // namespace offline_pages |