| // 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/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/archive_manager.h" |
| #include "components/offline_pages/client_namespace_constants.h" |
| #include "components/offline_pages/client_policy_controller.h" |
| #include "components/offline_pages/offline_page_item.h" |
| #include "components/offline_pages/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; |
| 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( |
| const std::map<int64_t, OfflinePageItem>& offline_pages, |
| const OfflinePageItem& offline_page, |
| const base::Time& save_time) { |
| // 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 (offline_page.client_id.name_space == kDownloadNamespace) { |
| 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 GURL& url, |
| const ClientId& client_id, |
| int64_t proposed_offline_id, |
| 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(url)) { |
| InformSavePageDone(callback, SavePageResult::SKIPPED, 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, client_id, |
| kInvalidOfflineId); |
| return; |
| } |
| |
| // If we already have an offline id, use it. If not, generate one. |
| if (proposed_offline_id == 0l) |
| proposed_offline_id = GenerateOfflineId(); |
| |
| archiver->CreateArchive( |
| archives_dir_, proposed_offline_id, |
| base::Bind(&OfflinePageModelImpl::OnCreateArchiveDone, |
| weak_ptr_factory_.GetWeakPtr(), url, proposed_offline_id, |
| client_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() || iter->second.IsExpired()) |
| 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() && !iter->second.IsExpired()) { |
| 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::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 (!IsUserRequestedPage(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) { |
| RunWhenLoaded( |
| base::Bind(&OfflinePageModelImpl::CheckPagesExistOfflineAfterLoadDone, |
| weak_ptr_factory_.GetWeakPtr(), urls, callback)); |
| } |
| |
| void OfflinePageModelImpl::CheckPagesExistOfflineAfterLoadDone( |
| const std::set<GURL>& urls, |
| const CheckPagesExistOfflineCallback& callback) { |
| DCHECK(is_loaded_); |
| CheckPagesExistOfflineResult result; |
| for (const auto& id_page_pair : offline_pages_) { |
| // TODO(dewittj): Remove the "Last N" restriction in favor of a better query |
| // interface. See https://crbug.com/622763 for information. |
| if (id_page_pair.second.IsExpired() || |
| id_page_pair.second.client_id.name_space == kLastNNamespace) |
| continue; |
| auto iter = urls.find(id_page_pair.second.url); |
| if (iter != urls.end()) |
| result.insert(*iter); |
| } |
| callback.Run(result); |
| } |
| |
| void OfflinePageModelImpl::GetAllPages( |
| const MultipleOfflinePageItemCallback& callback) { |
| RunWhenLoaded(base::Bind(&OfflinePageModelImpl::GetAllPagesAfterLoadDone, |
| weak_ptr_factory_.GetWeakPtr(), GetAllPageMode::ALL, |
| callback)); |
| } |
| |
| void OfflinePageModelImpl::GetAllPagesWithExpired( |
| const MultipleOfflinePageItemCallback& callback) { |
| RunWhenLoaded(base::Bind(&OfflinePageModelImpl::GetAllPagesAfterLoadDone, |
| weak_ptr_factory_.GetWeakPtr(), |
| GetAllPageMode::ALL_WITH_EXPIRED, callback)); |
| } |
| |
| void OfflinePageModelImpl::GetAllPagesAfterLoadDone( |
| GetAllPageMode mode, |
| const MultipleOfflinePageItemCallback& callback) const { |
| DCHECK(is_loaded_); |
| |
| MultipleOfflinePageItemResult offline_pages; |
| for (const auto& id_page_pair : offline_pages_) { |
| if (mode == GetAllPageMode::ALL_WITH_EXPIRED || |
| !id_page_pair.second.IsExpired()) |
| offline_pages.push_back(id_page_pair.second); |
| } |
| |
| callback.Run(offline_pages); |
| } |
| |
| 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 { |
| 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. |
| // TODO(bburns): actually use an index rather than linear scan. |
| for (const auto& id_page_pair : offline_pages_) { |
| if (id_page_pair.second.client_id == client_id && |
| !id_page_pair.second.IsExpired()) { |
| results.push_back(id_page_pair.second.offline_id); |
| } |
| } |
| return results; |
| } |
| |
| void OfflinePageModelImpl::GetPageByOfflineId( |
| int64_t offline_id, |
| const SingleOfflinePageItemCallback& callback) { |
| RunWhenLoaded( |
| base::Bind(&OfflinePageModelImpl::GetPageByOfflineIdWhenLoadDone, |
| weak_ptr_factory_.GetWeakPtr(), offline_id, callback)); |
| } |
| |
| void OfflinePageModelImpl::GetPageByOfflineIdWhenLoadDone( |
| int64_t offline_id, |
| const SingleOfflinePageItemCallback& callback) const { |
| callback.Run(MaybeGetPageByOfflineId(offline_id)); |
| } |
| |
| const OfflinePageItem* OfflinePageModelImpl::MaybeGetPageByOfflineId( |
| int64_t offline_id) const { |
| const auto iter = offline_pages_.find(offline_id); |
| return iter != offline_pages_.end() && !iter->second.IsExpired() |
| ? &(iter->second) |
| : nullptr; |
| } |
| |
| void OfflinePageModelImpl::GetPageByOfflineURL( |
| const GURL& offline_url, |
| const SingleOfflinePageItemCallback& callback) { |
| RunWhenLoaded( |
| base::Bind(&OfflinePageModelImpl::GetPageByOfflineURLWhenLoadDone, |
| weak_ptr_factory_.GetWeakPtr(), offline_url, callback)); |
| } |
| |
| void OfflinePageModelImpl::GetPageByOfflineURLWhenLoadDone( |
| const GURL& offline_url, |
| const SingleOfflinePageItemCallback& callback) const { |
| // Getting pages by offline URL does not exclude expired pages, as the caller |
| // already holds the offline URL and simply needs to look up a corresponding |
| // online URL. |
| const OfflinePageItem* result = nullptr; |
| |
| for (const auto& id_page_pair : offline_pages_) { |
| if (id_page_pair.second.GetOfflineURL() == offline_url) { |
| result = &id_page_pair.second; |
| break; |
| } |
| } |
| |
| callback.Run(result); |
| } |
| |
| const OfflinePageItem* OfflinePageModelImpl::MaybeGetPageByOfflineURL( |
| const GURL& offline_url) const { |
| // Getting pages by offline URL does not exclude expired pages, as the caller |
| // already holds the offline URL and simply needs to look up a corresponding |
| // online URL. |
| for (const auto& id_page_pair : offline_pages_) { |
| if (id_page_pair.second.GetOfflineURL() == offline_url) |
| return &(id_page_pair.second); |
| } |
| return nullptr; |
| } |
| |
| void OfflinePageModelImpl::GetPagesByOnlineURL( |
| const GURL& online_url, |
| const MultipleOfflinePageItemCallback& callback) { |
| RunWhenLoaded( |
| base::Bind(&OfflinePageModelImpl::GetPagesByOnlineURLWhenLoadDone, |
| weak_ptr_factory_.GetWeakPtr(), online_url, callback)); |
| } |
| |
| void OfflinePageModelImpl::GetPagesByOnlineURLWhenLoadDone( |
| const GURL& online_url, |
| const MultipleOfflinePageItemCallback& callback) const { |
| std::vector<OfflinePageItem> result; |
| |
| for (const auto& id_page_pair : offline_pages_) { |
| if (id_page_pair.second.url == online_url && |
| !id_page_pair.second.IsExpired()) { |
| result.push_back(id_page_pair.second); |
| } |
| } |
| |
| callback.Run(result); |
| } |
| |
| const OfflinePageItem* OfflinePageModelImpl::MaybeGetBestPageForOnlineURL( |
| const GURL& online_url) const { |
| const OfflinePageItem* result = nullptr; |
| for (const auto& id_page_pair : offline_pages_) { |
| if (id_page_pair.second.url == online_url && |
| !id_page_pair.second.IsExpired()) { |
| if (!result || id_page_pair.second.creation_time > result->creation_time) |
| result = &(id_page_pair.second); |
| } |
| } |
| return result; |
| } |
| |
| void OfflinePageModelImpl::CheckMetadataConsistency() { |
| DCHECK(is_loaded_); |
| archive_manager_->GetAllArchives( |
| base::Bind(&OfflinePageModelImpl::CheckMetadataConsistencyForArchivePaths, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void OfflinePageModelImpl::ExpirePages( |
| const std::vector<int64_t>& offline_ids, |
| const base::Time& expiration_time, |
| const base::Callback<void(bool)>& callback) { |
| std::vector<base::FilePath> paths_to_delete; |
| std::vector<OfflinePageItem> items_to_update; |
| for (int64_t offline_id : offline_ids) { |
| auto iter = offline_pages_.find(offline_id); |
| if (iter == offline_pages_.end()) |
| continue; |
| |
| OfflinePageItem offline_page = iter->second; |
| paths_to_delete.push_back(offline_page.file_path); |
| offline_page.expiration_time = expiration_time; |
| |
| items_to_update.push_back(offline_page); |
| } |
| |
| store_->UpdateOfflinePages( |
| items_to_update, |
| base::Bind(&OfflinePageModelImpl::OnExpirePageDone, |
| weak_ptr_factory_.GetWeakPtr(), expiration_time)); |
| |
| if (paths_to_delete.empty()) { |
| callback.Run(true); |
| return; |
| } |
| archive_manager_->DeleteMultipleArchives(paths_to_delete, callback); |
| } |
| |
| void OfflinePageModelImpl::OnExpirePageDone( |
| const base::Time& expiration_time, |
| std::unique_ptr<OfflinePagesUpdateResult> result) { |
| UMA_HISTOGRAM_BOOLEAN("OfflinePages.ExpirePage.StoreUpdateResult", |
| result->updated_items.size() > 0); |
| for (const auto& expired_page : result->updated_items) { |
| const auto& iter = offline_pages_.find(expired_page.offline_id); |
| if (iter == offline_pages_.end()) |
| continue; |
| |
| iter->second.expiration_time = expiration_time; |
| ClientId client_id = iter->second.client_id; |
| offline_event_logger_.RecordPageExpired( |
| std::to_string(expired_page.offline_id)); |
| base::HistogramBase* histogram = base::Histogram::FactoryGet( |
| AddHistogramSuffix(client_id, "OfflinePages.ExpirePage.PageLifetime"), |
| 1, base::TimeDelta::FromDays(30).InMinutes(), 50, |
| base::HistogramBase::kUmaTargetedHistogramFlag); |
| histogram->Add((expiration_time - iter->second.creation_time).InMinutes()); |
| histogram = base::Histogram::FactoryGet( |
| AddHistogramSuffix(client_id, |
| "OfflinePages.ExpirePage.TimeSinceLastAccess"), |
| 1, base::TimeDelta::FromDays(30).InMinutes(), 50, |
| base::HistogramBase::kUmaTargetedHistogramFlag); |
| histogram->Add( |
| (expiration_time - iter->second.last_access_time).InMinutes()); |
| } |
| } |
| |
| 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 GURL& requested_url, |
| int64_t offline_id, |
| const ClientId& client_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 (requested_url != url) { |
| DVLOG(1) << "Saved URL does not match requested URL."; |
| // TODO(fgorski): We have created an archive for a wrong URL. It should be |
| // deleted from here, once archiver has the right functionality. |
| InformSavePageDone(callback, SavePageResult::ARCHIVE_CREATION_FAILED, |
| client_id, offline_id); |
| DeletePendingArchiver(archiver); |
| return; |
| } |
| |
| if (archiver_result != ArchiverResult::SUCCESSFULLY_CREATED) { |
| SavePageResult result = ToSavePageResult(archiver_result); |
| InformSavePageDone(callback, result, client_id, offline_id); |
| DeletePendingArchiver(archiver); |
| return; |
| } |
| OfflinePageItem offline_page_item(url, offline_id, client_id, file_path, |
| file_size, start_time); |
| offline_page_item.title = title; |
| store_->AddOfflinePage(offline_page_item, |
| base::Bind(&OfflinePageModelImpl::OnAddOfflinePageDone, |
| weak_ptr_factory_.GetWeakPtr(), archiver, |
| callback, offline_page_item)); |
| } |
| |
| void OfflinePageModelImpl::OnAddOfflinePageDone( |
| OfflinePageArchiver* archiver, |
| 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(offline_pages_, offline_page, |
| GetCurrentTime()); |
| offline_event_logger_.RecordPageSaved( |
| offline_page.client_id.name_space, offline_page.url.spec(), |
| std::to_string(offline_page.offline_id)); |
| } else if (status == ItemActionStatus::ALREADY_EXISTS) { |
| 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(); |
| } |
| |
| DeletePendingArchiver(archiver); |
| FOR_EACH_OBSERVER(Observer, observers_, OfflinePageModelChanged(this)); |
| } |
| |
| 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); |
| |
| store_->GetOfflinePages(base::Bind(&OfflinePageModelImpl::OnLoadDone, |
| weak_ptr_factory_.GetWeakPtr(), |
| start_time)); |
| } |
| |
| void OfflinePageModelImpl::OnLoadDone( |
| const base::TimeTicks& start_time, |
| OfflinePageMetadataStore::LoadStatus load_status, |
| const std::vector<OfflinePageItem>& offline_pages) { |
| DCHECK(!is_loaded_); |
| is_loaded_ = true; |
| |
| // TODO(jianli): rebuild the store upon failure. |
| |
| if (load_status == OfflinePageMetadataStore::LOAD_SUCCEEDED) |
| CacheLoadedData(offline_pages); |
| |
| UMA_HISTOGRAM_TIMES("OfflinePages.Model.ConstructionToLoadedEventTime", |
| base::TimeTicks::Now() - start_time); |
| |
| // Create Storage Manager. |
| storage_manager_.reset(new OfflinePageStorageManager( |
| this, GetPolicyController(), archive_manager_.get())); |
| |
| // Run all the delayed tasks. |
| for (const auto& delayed_task : delayed_tasks_) |
| delayed_task.Run(); |
| delayed_tasks_.clear(); |
| |
| FOR_EACH_OBSERVER(Observer, observers_, OfflinePageModelLoaded(this)); |
| |
| CheckMetadataConsistency(); |
| |
| base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( |
| FROM_HERE, base::Bind(&OfflinePageModelImpl::ClearStorageIfNeeded, |
| weak_ptr_factory_.GetWeakPtr(), |
| base::Bind(&OfflinePageModelImpl::OnStorageCleared, |
| weak_ptr_factory_.GetWeakPtr())), |
| kStorageManagerStartingDelay); |
| } |
| |
| 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; |
| GetPagesByOnlineURL( |
| offline_page.url, |
| 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) { |
| // TODO(romax) Add UMAs for failure cases. |
| PostClearStorageIfNeededTask(); |
| } |
| |
| void OfflinePageModelImpl::DeletePendingArchiver( |
| OfflinePageArchiver* archiver) { |
| pending_archivers_.erase(std::find(pending_archivers_.begin(), |
| pending_archivers_.end(), 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(std::to_string(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_EACH_OBSERVER(Observer, observers_, |
| OfflinePageDeleted(page.offline_id, page.client_id)); |
| } |
| |
| // TODO(fgorski): React the FAILED_INITIALIZATION, FAILED_RESET here. |
| // TODO(fgorski): We need a better callback interface for the Remove action on |
| // the this class. Currently removing an item that does not exist is |
| // considered a success, but not called out as such to the caller. |
| 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) { |
| ExpirePagesMissingArchiveFile(archive_paths); |
| DeleteOrphanedArchives(archive_paths); |
| } |
| |
| void OfflinePageModelImpl::ExpirePagesMissingArchiveFile( |
| 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; |
| |
| ExpirePages( |
| ids_of_pages_missing_archive_file, GetCurrentTime(), |
| base::Bind(&OfflinePageModelImpl::OnExpirePagesMissingArchiveFileDone, |
| weak_ptr_factory_.GetWeakPtr(), |
| ids_of_pages_missing_archive_file)); |
| } |
| |
| void OfflinePageModelImpl::OnExpirePagesMissingArchiveFileDone( |
| const std::vector<int64_t>& offline_ids, |
| bool success) { |
| UMA_HISTOGRAM_COUNTS("OfflinePages.Consistency.PagesMissingArchiveFileCount", |
| static_cast<int32_t>(offline_ids.size())); |
| UMA_HISTOGRAM_BOOLEAN( |
| "OfflinePages.Consistency.ExpirePagesMissingArchiveFileResult", success); |
| } |
| |
| 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) { |
| storage_manager_->ClearPagesIfNeeded(callback); |
| } |
| |
| void OfflinePageModelImpl::OnStorageCleared(size_t expired_page_count, |
| ClearStorageResult result) { |
| UMA_HISTOGRAM_ENUMERATION("OfflinePages.ClearStorageResult", |
| static_cast<int>(result), |
| static_cast<int>(ClearStorageResult::RESULT_COUNT)); |
| if (expired_page_count > 0) { |
| UMA_HISTOGRAM_COUNTS("OfflinePages.ExpirePage.BatchSize", |
| static_cast<int32_t>(expired_page_count)); |
| } |
| } |
| |
| void OfflinePageModelImpl::PostClearStorageIfNeededTask() { |
| base::ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, base::Bind(&OfflinePageModelImpl::ClearStorageIfNeeded, |
| weak_ptr_factory_.GetWeakPtr(), |
| base::Bind(&OfflinePageModelImpl::OnStorageCleared, |
| weak_ptr_factory_.GetWeakPtr()))); |
| } |
| |
| bool OfflinePageModelImpl::IsUserRequestedPage( |
| const OfflinePageItem& offline_page) const { |
| return (offline_page.client_id.name_space == kAsyncNamespace || |
| offline_page.client_id.name_space == kDownloadNamespace); |
| } |
| |
| 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 |