| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/download/bubble/download_bubble_update_service.h" |
| |
| #include <iterator> |
| #include <optional> |
| #include <tuple> |
| |
| #include "base/functional/bind.h" |
| #include "base/logging.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/content_index/content_index_provider_impl.h" |
| #include "chrome/browser/download/bubble/download_bubble_ui_controller.h" |
| #include "chrome/browser/download/bubble/download_bubble_update_service_factory.h" |
| #include "chrome/browser/download/bubble/download_bubble_utils.h" |
| #include "chrome/browser/download/bubble/download_display_controller.h" |
| #include "chrome/browser/download/download_core_service.h" |
| #include "chrome/browser/download/download_core_service_factory.h" |
| #include "chrome/browser/download/download_crx_util.h" |
| #include "chrome/browser/download/download_item_model.h" |
| #include "chrome/browser/download/download_item_web_app_data.h" |
| #include "chrome/browser/download/download_ui_model.h" |
| #include "chrome/browser/download/offline_item_model_manager.h" |
| #include "chrome/browser/download/offline_item_model_manager_factory.h" |
| #include "chrome/browser/download/offline_item_utils.h" |
| #include "chrome/browser/offline_items_collection/offline_content_aggregator_factory.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/profiles/profile_key.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_finder.h" |
| #include "chrome/browser/ui/browser_window.h" |
| #include "chrome/browser/ui/web_applications/app_browser_controller.h" |
| #include "components/download/content/public/all_download_item_notifier.h" |
| #include "components/download/public/common/download_item.h" |
| #include "components/offline_items_collection/core/offline_content_provider.h" |
| #include "components/offline_items_collection/core/offline_item.h" |
| #include "content/public/browser/download_manager.h" |
| |
| namespace { |
| |
| using ::offline_items_collection::ContentId; |
| using ::offline_items_collection::OfflineContentProvider; |
| using ::offline_items_collection::OfflineItem; |
| using ::offline_items_collection::OfflineItemState; |
| using DownloadState = download::DownloadItem::DownloadState; |
| using DownloadUIModelPtr = DownloadUIModel::DownloadUIModelPtr; |
| using ItemSortKey = DownloadBubbleUpdateService::ItemSortKey; |
| template <typename Id, typename Item> |
| using IterMap = DownloadBubbleUpdateService::IterMap<Id, Item>; |
| using ProgressInfo = DownloadDisplay::ProgressInfo; |
| template <typename Item> |
| using SortedItems = DownloadBubbleUpdateService::SortedItems<Item>; |
| |
| // Show up to 30 items in total by default. |
| constexpr size_t kDefaultMaxNumItemsToShow = 30u; |
| // Cache a few more items of each type than we will return from |
| // GetAllModelsToDisplay. This gives us some wiggle room and makes it more |
| // likely that we'll return enough items before backfilling. |
| constexpr size_t kDefaultExtraItemsToCache = 30u; |
| // Amount of time to show an item in the bubble. Items older than this duration |
| // ago will be pruned. |
| constexpr base::TimeDelta kShowItemInBubbleDuration = base::Hours(24); |
| // Don't send the "download started" notification for an extension or theme |
| // (crx) download until 2 seconds after it has begun. If it is a small download |
| // that finishes in under 2 seconds, the download UI does not show at all. If it |
| // is a large download that takes longer than 2 seconds, show the UI so that the |
| // user knows Chrome is working on it. |
| constexpr base::TimeDelta kCrxShowNewItemDelay = base::Seconds(2); |
| // Limit the size of the |delayed_crx_guids_| set so it doesn't grow |
| // unboundedly. It is unlikely that the user would have 20 active crx downloads |
| // simultaneously. |
| constexpr int kMaxDelayedCrxGuids = 20; |
| |
| template <typename Item> |
| ItemSortKey::State GetState(const Item& item) { |
| if (IsItemInProgress(item)) { |
| return IsItemPaused(item) ? ItemSortKey::kInProgressPaused |
| : ItemSortKey::kInProgressActive; |
| } |
| return ItemSortKey::kNotInProgress; |
| } |
| |
| template <typename Item> |
| ItemSortKey GetSortKey(const Item& item) { |
| return ItemSortKey{GetState(item), GetItemStartTime(item)}; |
| } |
| |
| // Helper to get an iterator to the last element in the cache. The cache |
| // must not be empty. |
| template <typename Item> |
| SortedItems<Item>::const_iterator GetLastIter(const SortedItems<Item>& cache) { |
| CHECK(!cache.empty()); |
| auto it = cache.end(); |
| return std::prev(it); |
| } |
| |
| // Returns the earliest creation time for which we will show items in the |
| // bubble. |
| base::Time GetCutoffTime() { |
| return base::Time::Now() - kShowItemInBubbleDuration; |
| } |
| |
| // Updates the count of received vs total bytes. Returns whether progress is |
| // certain. |
| bool AddItemProgress(int64_t item_received_bytes, |
| int64_t item_total_bytes, |
| int& in_progress_items, |
| int64_t& received_bytes, |
| int64_t& total_bytes) { |
| ++in_progress_items; |
| if (item_total_bytes <= 0) { |
| // Progress is uncertain: there may or may not be more data coming down this |
| // pipe. |
| return false; |
| } |
| received_bytes += item_received_bytes; |
| total_bytes += item_total_bytes; |
| return true; |
| } |
| |
| bool ShouldIncludeModel(const DownloadUIModel* model, base::Time cutoff_time) { |
| return DownloadUIModelIsRecent(model, cutoff_time) && |
| model->ShouldShowInBubble(); |
| } |
| |
| // Returns whether |model| was eligible to be added to |models|, regardless of |
| // whether it was actually added. |
| bool MaybeAddModel(DownloadUIModelPtr model, |
| base::Time cutoff_time, |
| size_t max_num_models, |
| std::vector<DownloadUIModelPtr>& models) { |
| if (!ShouldIncludeModel(model.get(), cutoff_time)) { |
| model->SetActionedOn(true); |
| return false; |
| } |
| if (models.size() < max_num_models) { |
| models.push_back(std::move(model)); |
| } |
| return true; |
| } |
| |
| // For GetAllModelsToDisplay()'s iteration over the merged caches, don't stop |
| // until all models have been processed. |
| bool NeverStop() { |
| return false; |
| } |
| |
| // `update_is_for_model` is whether the current call to this function was |
| // triggered on behalf of `model`. |
| void UpdateInfoForModel(const DownloadUIModel& model, |
| bool update_is_for_model, |
| base::Time cutoff_time, |
| DownloadBubbleDisplayInfo& info) { |
| if (!ShouldIncludeModel(&model, cutoff_time)) { |
| return; |
| } |
| ++info.all_models_size; |
| if (model.GetDangerType() == download::DOWNLOAD_DANGER_TYPE_ASYNC_SCANNING && |
| model.GetState() != download::DownloadItem::CANCELLED) { |
| info.has_deep_scanning = true; |
| } |
| if (!model.WasActionedOn()) { |
| info.has_unactioned = true; |
| } |
| if (IsModelInProgress(&model)) { |
| ++info.in_progress_count; |
| if (model.IsPaused()) { |
| ++info.paused_count; |
| } |
| } else { |
| base::Time cur_completed_time = model.GetEndTime(); |
| if (cur_completed_time.is_null() && update_is_for_model && |
| model.GetState() != download::DownloadItem::CANCELLED) { |
| // Given that we consider dangerous/insecure downloads to be complete, the |
| // completion time should reflect the time they were marked as |
| // dangerous/insecure. Since download is still technically IN_PROGRESS in |
| // this scenario and thus has a null end time, we just use the current |
| // time based on the assumption that a download in a dangerous/insecure |
| // state does not receive further updates besides cancellation. |
| cur_completed_time = base::Time::Now(); |
| } |
| info.last_completed_time = |
| std::max(info.last_completed_time, cur_completed_time); |
| } |
| } |
| |
| bool BrowserMatchesWebAppData(const Browser* browser, |
| const DownloadItemWebAppData* data) { |
| return data != nullptr |
| ? web_app::AppBrowserController::IsForWebApp(browser, data->id()) |
| : !web_app::AppBrowserController::IsWebApp(browser); |
| } |
| |
| } // namespace |
| |
| bool DownloadBubbleUpdateService::ItemSortKey::operator<( |
| const DownloadBubbleUpdateService::ItemSortKey& other) const { |
| if (state < other.state) { |
| return true; |
| } else if (state > other.state) { |
| return false; |
| } |
| return start_time > other.start_time; |
| } |
| |
| bool DownloadBubbleUpdateService::ItemSortKey::operator>( |
| const DownloadBubbleUpdateService::ItemSortKey& other) const { |
| return !(*this == other || *this < other); |
| } |
| |
| // static |
| DownloadBubbleUpdateService::ItemSortKey |
| DownloadBubbleUpdateService::ItemSortKey::Min() { |
| return ItemSortKey{kInProgressActive, base::Time::Max()}; |
| } |
| |
| DownloadBubbleUpdateService::CacheManager::CacheManager( |
| DownloadBubbleUpdateService* update_service) |
| : update_service_(update_service) { |
| CHECK(update_service); |
| } |
| |
| DownloadBubbleUpdateService::CacheManager::~CacheManager() = default; |
| |
| DownloadBubbleUpdateService::DownloadBubbleUpdateService(Profile* profile) |
| : profile_(profile), |
| original_profile_(IsProfileOtr() ? profile_->GetOriginalProfile() |
| : nullptr), |
| main_cache_(this) { |
| offline_content_provider_observation_.Observe( |
| OfflineContentAggregatorFactory::GetForKey(profile_->GetProfileKey())); |
| } |
| |
| DownloadBubbleUpdateService::~DownloadBubbleUpdateService() = default; |
| |
| void DownloadBubbleUpdateService::Shutdown() { |
| offline_content_provider_observation_.Reset(); |
| is_shut_down_ = true; |
| } |
| |
| bool DownloadBubbleUpdateService::IsShutDown() const { |
| return is_shut_down_; |
| } |
| |
| bool DownloadBubbleUpdateService::IsProfileOtr() const { |
| return profile_->IsOffTheRecord(); |
| } |
| |
| size_t DownloadBubbleUpdateService::GetMaxNumItemsToShow() const { |
| size_t max = |
| max_num_items_to_show_for_testing_.value_or(kDefaultMaxNumItemsToShow); |
| CHECK_GE(max, 2u); |
| return max; |
| } |
| |
| size_t DownloadBubbleUpdateService::GetNumItemsToCache() const { |
| return GetMaxNumItemsToShow() + |
| extra_items_to_cache_for_testing_.value_or(kDefaultExtraItemsToCache); |
| } |
| |
| size_t DownloadBubbleUpdateService::CacheManager::GetMaxNumItemsToShow() const { |
| return update_service_->GetMaxNumItemsToShow(); |
| } |
| |
| size_t DownloadBubbleUpdateService::CacheManager::GetNumItemsToCache() const { |
| return update_service_->GetNumItemsToCache(); |
| } |
| |
| bool DownloadBubbleUpdateService::CacheManager::IsDownloadItemCacheAtMax() |
| const { |
| CHECK(download_items_.size() <= GetNumItemsToCache()); |
| return download_items_.size() == GetNumItemsToCache(); |
| } |
| |
| bool DownloadBubbleUpdateService::CacheManager::IsOfflineItemCacheAtMax() |
| const { |
| CHECK(offline_items_.size() <= GetNumItemsToCache()); |
| return offline_items_.size() == GetNumItemsToCache(); |
| } |
| |
| DownloadBubbleUpdateService::CacheManager& |
| DownloadBubbleUpdateService::GetCacheForWebApp(const webapps::AppId& app_id) { |
| auto it = web_app_caches_.find(app_id); |
| if (it == web_app_caches_.end()) { |
| // Create a new CacheManager for this |app_id|. |
| it = web_app_caches_.emplace(app_id, this).first; |
| } |
| return it->second; |
| } |
| |
| const DownloadBubbleUpdateService::CacheManager* |
| DownloadBubbleUpdateService::GetExistingCacheForWebApp( |
| const webapps::AppId& app_id) const { |
| if (auto it = web_app_caches_.find(app_id); it != web_app_caches_.end()) { |
| return &it->second; |
| } |
| return nullptr; |
| } |
| |
| DownloadBubbleUpdateService::CacheManager* |
| DownloadBubbleUpdateService::GetExistingCacheForWebApp( |
| const webapps::AppId& app_id) { |
| return const_cast<CacheManager*>( |
| std::as_const(*this).GetExistingCacheForWebApp(app_id)); |
| } |
| |
| DownloadBubbleUpdateService::CacheManager& |
| DownloadBubbleUpdateService::GetCacheForItem(download::DownloadItem* item) { |
| auto* web_app_data = DownloadItemWebAppData::Get(item); |
| if (web_app_data == nullptr) { |
| return main_cache_; |
| } |
| return GetCacheForWebApp(web_app_data->id()); |
| } |
| |
| std::vector<DownloadBubbleUpdateService::CacheManager*> |
| DownloadBubbleUpdateService::GetAllCacheManagers() { |
| std::vector<CacheManager*> cache_managers; |
| cache_managers.push_back(&main_cache_); |
| for (auto& [web_app_id, cache_manager] : web_app_caches_) { |
| cache_managers.push_back(&cache_manager); |
| } |
| return cache_managers; |
| } |
| |
| void DownloadBubbleUpdateService::ObserveDownloadHistory() { |
| // If OTR, this is the original profile. Otherwise, this is just the profile |
| // itself. |
| Profile* profile = profile_->GetOriginalProfile(); |
| if (DownloadCoreService* dcs = |
| DownloadCoreServiceFactory::GetForBrowserContext(profile); |
| dcs && dcs->GetDownloadHistory()) { |
| download_history_observation_.Observe(dcs->GetDownloadHistory()); |
| } |
| } |
| |
| void DownloadBubbleUpdateService::Initialize( |
| content::DownloadManager* manager) { |
| CHECK(manager); |
| CHECK(!download_item_notifier_); |
| |
| // This is safe to do here because we know the DownloadManager has been |
| // created by now. If we did this earlier, then it might trigger early |
| // initialization of the DownloadManager and ChromeDownloadManagerDelegate. |
| ObserveDownloadHistory(); |
| |
| // Assume we have an original profile and it has an OTR profile. |
| // If the original profile's DownloadBubbleUpdateService is Initialize()'d |
| // already when this function is invoked on the OTR profile's |
| // DownloadBubbleUpdateService, we set the OTR profile's |
| // DownloadBubbleUpdateService's |original_download_item_notifier_| in the |
| // 'if' block below. If the original profile's DownloadBubbleUpdateService is |
| // not yet initialized when this function is invoked on the OTR profile's |
| // DownloadBubbleUpdateService, we will set the OTR profile's |
| // DownloadBubbleUpdateService's |original_download_item_notifier_| when the |
| // original profile's DownloadBubbleUpdateService does become Initialize()'d, |
| // in the 'else' block below (which will trigger re-intialization of the |
| // download item cache). |
| if (profile_->IsOffTheRecord()) { |
| DownloadBubbleUpdateService* original_update_service = |
| DownloadBubbleUpdateServiceFactory::GetForProfile(original_profile_); |
| content::DownloadManager* original_download_manager = |
| original_update_service ? original_update_service->GetDownloadManager() |
| : nullptr; |
| if (original_download_manager) { |
| InitializeOriginalNotifier(original_download_manager); |
| } |
| } else { |
| for (Profile* otr_profile : profile_->GetAllOffTheRecordProfiles()) { |
| DownloadBubbleUpdateServiceFactory::GetForProfile(otr_profile) |
| ->InitializeOriginalNotifier(manager); |
| } |
| } |
| download_item_notifier_ = |
| std::make_unique<download::AllDownloadItemNotifier>(manager, this); |
| // As long as we have a notifier for this profile, we can initialize the cache |
| // with the current profile's downloads. If we get an original manager in the |
| // future, we will initialize from scratch at that time. |
| InitializeDownloadItemsCache(); |
| StartInitializeOfflineItemsCache(); |
| } |
| |
| void DownloadBubbleUpdateService::InitializeOriginalNotifier( |
| content::DownloadManager* manager) { |
| CHECK(profile_->IsOffTheRecord()); |
| CHECK(manager); |
| if (original_download_item_notifier_) { |
| return; |
| } |
| original_download_item_notifier_ = |
| std::make_unique<download::AllDownloadItemNotifier>(manager, this); |
| // Reset the download items cache, now that we have an original |
| // DownloadManager to pull from. |
| if (download_item_notifier_) { |
| InitializeDownloadItemsCache(); |
| } |
| } |
| |
| content::DownloadManager* DownloadBubbleUpdateService::GetDownloadManager() { |
| return download_item_notifier_ ? download_item_notifier_->GetManager() |
| : nullptr; |
| } |
| |
| bool DownloadBubbleUpdateService::IsInitialized() const { |
| return download_item_notifier_ && offline_items_initialized_; |
| } |
| |
| bool DownloadBubbleUpdateService::CacheManager::GetAllModelsToDisplay( |
| std::vector<DownloadUIModelPtr>& models, |
| bool force_backfill_download_items) { |
| #if DCHECK_IS_ON() |
| ConsistencyCheckCaches(); |
| #endif // DCHECK_IS_ON() |
| |
| base::Time cutoff_time = GetCutoffTime(); |
| models.clear(); |
| // If the caches are at max capacity, and we prune some items that are too |
| // old, we may need to backfill items. |
| bool download_items_cache_was_at_max = IsDownloadItemCacheAtMax(); |
| bool offline_items_cache_was_at_max = IsOfflineItemCacheAtMax(); |
| bool download_item_pruned = false; |
| bool offline_item_pruned = false; |
| |
| // Merge the two sorted lists, while pruning the expired items. Since the |
| // criteria for pruning requires the model, to avoid unnecessary creation and |
| // destruction of models, we collect the models to return and prune items in |
| // the same loop iteration. |
| IterateOverMergedCaches( |
| base::BindRepeating(&DownloadBubbleUpdateService::CacheManager:: |
| GetDownloadItemModelToDisplayOrPrune, |
| base::Unretained(this), cutoff_time, std::ref(models), |
| std::ref(download_item_pruned)), |
| base::BindRepeating(&DownloadBubbleUpdateService::CacheManager:: |
| GetOfflineItemModelToDisplayOrPrune, |
| base::Unretained(this), cutoff_time, std::ref(models), |
| std::ref(offline_item_pruned)), |
| base::BindRepeating(&NeverStop)); |
| |
| CHECK_LE(models.size(), GetMaxNumItemsToShow()); |
| |
| bool download_items_need_backfill = |
| download_item_pruned && download_items_cache_was_at_max; |
| bool offline_items_need_backfill = |
| offline_item_pruned && offline_items_cache_was_at_max; |
| |
| if (download_items_need_backfill) { |
| // A key that will sort before any other key. |
| ItemSortKey last_download_item_key = ItemSortKey::Min(); |
| if (!download_items_.empty()) { |
| last_download_item_key = GetLastIter(download_items_)->first; |
| } |
| if (force_backfill_download_items) { |
| // Get more items synchronously. |
| update_service_->BackfillDownloadItems(last_download_item_key); |
| AppendBackfilledDownloadItems(last_download_item_key, cutoff_time, |
| models); |
| download_items_need_backfill = false; |
| } else { |
| update_service_->StartBackfillDownloadItems(last_download_item_key); |
| } |
| } |
| |
| if (offline_items_need_backfill) { |
| // A key that will sort before any other key. |
| ItemSortKey last_offline_item_key = ItemSortKey::Min(); |
| if (!offline_items_.empty()) { |
| last_offline_item_key = GetLastIter(offline_items_)->first; |
| } |
| update_service_->StartBackfillOfflineItems(last_offline_item_key); |
| } |
| |
| #if DCHECK_IS_ON() |
| ConsistencyCheckCaches(); |
| #endif // DCHECK_IS_ON() |
| return models.size() == GetMaxNumItemsToShow() || |
| !(download_items_need_backfill || offline_items_need_backfill); |
| } |
| |
| void DownloadBubbleUpdateService::CacheManager:: |
| GetDownloadItemModelToDisplayOrPrune( |
| base::Time cutoff_time, |
| std::vector<DownloadUIModel::DownloadUIModelPtr>& models, |
| bool& download_item_pruned, |
| SortedDownloadItems::iterator& download_item_it) { |
| if (!MaybeAddDownloadItemModel(download_item_it->second, cutoff_time, |
| models)) { |
| download_item_it = RemoveItemFromCacheByIter( |
| download_item_it, download_items_, download_items_iter_map_); |
| download_item_pruned = true; |
| } else { |
| ++download_item_it; |
| } |
| } |
| |
| void DownloadBubbleUpdateService::CacheManager:: |
| GetOfflineItemModelToDisplayOrPrune( |
| base::Time cutoff_time, |
| std::vector<DownloadUIModel::DownloadUIModelPtr>& models, |
| bool& offline_item_pruned, |
| SortedOfflineItems::iterator& offline_item_it) { |
| if (!MaybeAddOfflineItemModel(offline_item_it->second, cutoff_time, models)) { |
| offline_item_it = RemoveItemFromCacheByIter(offline_item_it, offline_items_, |
| offline_items_iter_map_); |
| offline_item_pruned = true; |
| } else { |
| ++offline_item_it; |
| } |
| } |
| |
| bool DownloadBubbleUpdateService::GetAllModelsToDisplay( |
| std::vector<DownloadUIModelPtr>& models, |
| const webapps::AppId* web_app_id, |
| bool force_backfill_download_items) { |
| if (web_app_id == nullptr) { |
| return main_cache_.GetAllModelsToDisplay(models, |
| force_backfill_download_items); |
| } |
| return GetCacheForWebApp(*web_app_id) |
| .GetAllModelsToDisplay(models, force_backfill_download_items); |
| } |
| |
| const DownloadBubbleDisplayInfo& |
| DownloadBubbleUpdateService::CacheManager::GetDisplayInfo() const { |
| return display_info_; |
| } |
| |
| const DownloadBubbleDisplayInfo& DownloadBubbleUpdateService::GetDisplayInfo( |
| const webapps::AppId* web_app_id) { |
| if (web_app_id == nullptr) { |
| return main_cache_.GetDisplayInfo(); |
| } |
| if (const CacheManager* cache = GetExistingCacheForWebApp(*web_app_id); |
| cache != nullptr) { |
| return cache->GetDisplayInfo(); |
| } |
| return DownloadBubbleDisplayInfo::EmptyInfo(); |
| } |
| |
| void DownloadBubbleUpdateService::CacheManager::UpdateDisplayInfo( |
| const std::string& updating_for_item) { |
| #if DCHECK_IS_ON() |
| ConsistencyCheckCaches(); |
| #endif // DCHECK_IS_ON() |
| |
| // A new info is constructed from scratch based on the current cache contents. |
| DownloadBubbleDisplayInfo info; |
| base::Time cutoff_time = GetCutoffTime(); |
| |
| // Iterate over the two sorted caches (download items and offline items) in |
| // combined/merged sorted order. This is done in the same way as in |
| // GetAllItemsToDisplay() to ensure that the info most accurately represents |
| // the list of items that would be returned from that method. |
| IterateOverMergedCaches( |
| base::BindRepeating( |
| &DownloadBubbleUpdateService::CacheManager:: |
| UpdateDisplayInfoForDownloadItem, |
| base::Unretained(this), |
| base::optional_ref<const std::string>(updating_for_item), cutoff_time, |
| std::ref(info)), |
| base::BindRepeating(&DownloadBubbleUpdateService::CacheManager:: |
| UpdateDisplayInfoForOfflineItem, |
| base::Unretained(this), std::nullopt, cutoff_time, |
| std::ref(info)), |
| base::BindRepeating(&DownloadBubbleUpdateService::CacheManager:: |
| ShouldStopUpdatingDisplayInfo, |
| base::Unretained(this), std::ref(info))); |
| |
| display_info_ = info; |
| } |
| |
| void DownloadBubbleUpdateService::CacheManager::UpdateDisplayInfo( |
| const ContentId& updating_for_item) { |
| #if DCHECK_IS_ON() |
| ConsistencyCheckCaches(); |
| #endif // DCHECK_IS_ON() |
| |
| // A new info is constructed from scratch based on the current cache contents. |
| DownloadBubbleDisplayInfo info; |
| base::Time cutoff_time = GetCutoffTime(); |
| |
| // Iterate over the two sorted caches (download items and offline items) in |
| // combined/merged sorted order. This is done in the same way as in |
| // GetAllItemsToDisplay() to ensure that the info most accurately represents |
| // the list of items that would be returned from that method. |
| IterateOverMergedCaches( |
| base::BindRepeating(&DownloadBubbleUpdateService::CacheManager:: |
| UpdateDisplayInfoForDownloadItem, |
| base::Unretained(this), std::nullopt, cutoff_time, |
| std::ref(info)), |
| base::BindRepeating( |
| &DownloadBubbleUpdateService::CacheManager:: |
| UpdateDisplayInfoForOfflineItem, |
| base::Unretained(this), |
| base::optional_ref<const ContentId>(updating_for_item), cutoff_time, |
| std::ref(info)), |
| base::BindRepeating(&DownloadBubbleUpdateService::CacheManager:: |
| ShouldStopUpdatingDisplayInfo, |
| base::Unretained(this), std::ref(info))); |
| |
| display_info_ = info; |
| } |
| |
| void DownloadBubbleUpdateService::CacheManager:: |
| UpdateDisplayInfoForDownloadItem( |
| base::optional_ref<const std::string> updating_for_item, |
| base::Time cutoff_time, |
| DownloadBubbleDisplayInfo& info, |
| SortedDownloadItems::iterator& download_item_it) { |
| DownloadItemModel model( |
| download_item_it->second, |
| std::make_unique<DownloadUIModel::BubbleStatusTextBuilder>()); |
| bool update_is_for_model = |
| updating_for_item.has_value() && |
| *updating_for_item == GetItemId(download_item_it->second); |
| UpdateInfoForModel(model, update_is_for_model, cutoff_time, info); |
| ++download_item_it; |
| } |
| |
| void DownloadBubbleUpdateService::CacheManager::UpdateDisplayInfoForOfflineItem( |
| base::optional_ref<const ContentId> updating_for_item, |
| base::Time cutoff_time, |
| DownloadBubbleDisplayInfo& info, |
| SortedOfflineItems::iterator& offline_item_it) { |
| OfflineItemModel model( |
| update_service_->GetOfflineManager(), offline_item_it->second, |
| std::make_unique<DownloadUIModel::BubbleStatusTextBuilder>()); |
| bool update_is_for_model = |
| updating_for_item.has_value() && |
| *updating_for_item == GetItemId(offline_item_it->second); |
| UpdateInfoForModel(model, update_is_for_model, cutoff_time, info); |
| ++offline_item_it; |
| } |
| |
| bool DownloadBubbleUpdateService::CacheManager::ShouldStopUpdatingDisplayInfo( |
| const DownloadBubbleDisplayInfo& info) { |
| return info.all_models_size >= GetMaxNumItemsToShow(); |
| } |
| |
| void DownloadBubbleUpdateService::CacheManager::IterateOverMergedCaches( |
| base::RepeatingCallback<void(SortedDownloadItems::iterator&)> |
| download_item_action, |
| base::RepeatingCallback<void(SortedOfflineItems::iterator&)> |
| offline_item_action, |
| base::RepeatingCallback<bool()> should_stop) { |
| auto download_item_it = download_items_.begin(); |
| auto offline_item_it = offline_items_.begin(); |
| while (download_item_it != download_items_.end() || |
| offline_item_it != offline_items_.end()) { |
| // If the current download item sorts before the current offline item (or we |
| // are out of offline items), take the download item. |
| if (download_item_it != download_items_.end() && |
| (offline_item_it == offline_items_.end() || |
| download_item_it->first < offline_item_it->first)) { |
| download_item_action.Run(download_item_it); |
| } else { |
| // Else, the current offline item sorts before the current download item |
| // (or we are out of download items), so take the offline item. |
| offline_item_action.Run(offline_item_it); |
| } |
| if (should_stop.Run()) { |
| break; |
| } |
| } |
| } |
| |
| ProgressInfo DownloadBubbleUpdateService::CacheManager::GetProgressInfo() |
| const { |
| #if DCHECK_IS_ON() |
| ConsistencyCheckCaches(); |
| #endif // DCHECK_IS_ON() |
| |
| ProgressInfo info; |
| int in_progress_items = 0; |
| int64_t received_bytes = 0; |
| int64_t total_bytes = 0; |
| |
| base::Time cutoff_time = GetCutoffTime(); |
| |
| for (const auto& [key, item] : download_items_) { |
| if (key.state == ItemSortKey::kNotInProgress) { |
| break; |
| } |
| if (GetItemStartTime(item) < cutoff_time) { |
| continue; |
| } |
| // Note that operator&= does not short-circuit. |
| info.progress_certain &= |
| AddItemProgress(item->GetReceivedBytes(), item->GetTotalBytes(), |
| in_progress_items, received_bytes, total_bytes); |
| } |
| |
| for (const auto& [key, item] : offline_items_) { |
| if (key.state == ItemSortKey::kNotInProgress) { |
| break; |
| } |
| if (GetItemStartTime(item) < cutoff_time) { |
| continue; |
| } |
| // Note that operator&= does not short-circuit. |
| info.progress_certain &= |
| AddItemProgress(item.received_bytes, item.total_size_bytes, |
| in_progress_items, received_bytes, total_bytes); |
| } |
| |
| info.download_count = in_progress_items; |
| if (total_bytes > 0) { |
| info.progress_percentage = |
| base::ClampFloor(received_bytes * 100.0 / total_bytes); |
| } |
| return info; |
| } |
| |
| ProgressInfo DownloadBubbleUpdateService::GetProgressInfo( |
| const webapps::AppId* web_app_id) const { |
| if (web_app_id == nullptr) { |
| return main_cache_.GetProgressInfo(); |
| } |
| if (const CacheManager* cache = GetExistingCacheForWebApp(*web_app_id); |
| cache != nullptr) { |
| return cache->GetProgressInfo(); |
| } |
| return ProgressInfo{}; |
| } |
| |
| std::vector<std::u16string> DownloadBubbleUpdateService::CacheManager:: |
| TakeAccessibleAlertsForAnnouncement() { |
| std::vector<std::u16string> to_announce = |
| accessible_alerts_.TakeAlertsForAnnouncement(); |
| accessible_alerts_.GarbageCollect(); |
| return to_announce; |
| } |
| |
| std::vector<std::u16string> |
| DownloadBubbleUpdateService::TakeAccessibleAlertsForAnnouncement( |
| const webapps::AppId* web_app_id) { |
| if (web_app_id == nullptr) { |
| return main_cache_.TakeAccessibleAlertsForAnnouncement(); |
| } |
| if (CacheManager* cache = GetExistingCacheForWebApp(*web_app_id); |
| cache != nullptr) { |
| return cache->TakeAccessibleAlertsForAnnouncement(); |
| } |
| return std::vector<std::u16string>(); |
| } |
| |
| void DownloadBubbleUpdateService::OnDownloadCreated( |
| content::DownloadManager* manager, |
| download::DownloadItem* item) { |
| if (IsShutDown()) { |
| return; |
| } |
| CHECK(download_item_notifier_ || original_download_item_notifier_); |
| if (!download_item_notifier_) { |
| return; |
| } |
| if (download_crx_util::IsExtensionDownload(*item) && |
| delayed_crx_guids_.size() < kMaxDelayedCrxGuids) { |
| const std::string& guid = item->GetGuid(); |
| CHECK(!delayed_crx_guids_.contains(guid)); |
| delayed_crx_guids_.insert(guid); |
| base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce( |
| &DownloadBubbleUpdateService::OnDelayedCrxDownloadCreated, |
| weak_factory_.GetWeakPtr(), guid), |
| kCrxShowNewItemDelay); |
| return; |
| } |
| GetCacheForItem(item).MaybeAddDownloadItemToCache( |
| item, /*is_new=*/true, |
| /*maybe_add_alert=*/download_history_loaded_); |
| // NotifyWindowsOfDownloadItemAdded() is called from |
| // DownloadBubbleUIControllerDelegate for new non-crx downloads. |
| } |
| |
| void DownloadBubbleUpdateService::OnDelayedCrxDownloadCreated( |
| const std::string& guid) { |
| if (IsShutDown()) { |
| return; |
| } |
| CHECK(download_item_notifier_ || original_download_item_notifier_); |
| if (!download_item_notifier_) { |
| return; |
| } |
| // This assumes that for extension/theme downloads, the DownloadItem is |
| // removed from the DownloadManager upon completion. |
| download::DownloadItem* item = |
| download_item_notifier_->GetManager()->GetDownloadByGuid(guid); |
| if (item && !item->IsDone()) { |
| GetCacheForItem(item).MaybeAddDownloadItemToCache( |
| item, /*is_new=*/true, |
| /*maybe_add_alert=*/download_history_loaded_); |
| NotifyWindowsOfDownloadItemAdded(item); |
| } |
| size_t erased = delayed_crx_guids_.erase(guid); |
| CHECK_EQ(erased, 1u); |
| } |
| |
| void DownloadBubbleUpdateService::NotifyWindowsOfDownloadItemAdded( |
| download::DownloadItem* item) { |
| Browser* browser_to_show_animation = |
| FindBrowserToShowAnimation(item, profile_); |
| auto* web_app_data = DownloadItemWebAppData::Get(item); |
| for (Browser* browser : chrome::FindAllBrowsersWithProfile(profile_)) { |
| if (browser->window() && |
| browser->window()->GetDownloadBubbleUIController() && |
| BrowserMatchesWebAppData(browser, web_app_data)) { |
| browser->window()->GetDownloadBubbleUIController()->OnDownloadItemAdded( |
| item, /*may_show_animation=*/browser == browser_to_show_animation); |
| } |
| } |
| } |
| |
| void DownloadBubbleUpdateService::OnDownloadUpdated( |
| content::DownloadManager* manager, |
| download::DownloadItem* item) { |
| if (IsShutDown()) { |
| return; |
| } |
| CHECK(download_item_notifier_ || original_download_item_notifier_); |
| if (!download_item_notifier_) { |
| return; |
| } |
| // If the item is an extension or theme download waiting out its 2-second |
| // delay, don't show a UI update for it. |
| if (delayed_crx_guids_.contains(item->GetGuid())) { |
| return; |
| } |
| CacheManager& cache = GetCacheForItem(item); |
| // When persisted web app download items are restored from the history |
| // database, we first get a OnDownloadCreated() notification about the item |
| // without its DownloadItemWebAppData, causing the item to go into the main |
| // cache, followed by an OnDownloadUpdated() notification after the |
| // DownloadItemWebAppData is added. In order to keep the item in the |
| // appropriate cache for the web app, and NOT in the main cache, we must |
| // remove it from the main cache explicitly here. Note this assumes that an |
| // item's associated web app id never changes once it is tagged. |
| if (!IsMainCache(cache)) { |
| main_cache_.OnDownloadItemRemoved(item); |
| } |
| cache.OnDownloadItemUpdated(item); |
| |
| auto* web_app_data = DownloadItemWebAppData::Get(item); |
| for (Browser* browser : chrome::FindAllBrowsersWithProfile(profile_)) { |
| if (browser->window() && |
| browser->window()->GetDownloadBubbleUIController() && |
| BrowserMatchesWebAppData(browser, web_app_data)) { |
| browser->window()->GetDownloadBubbleUIController()->OnDownloadItemUpdated( |
| item); |
| } |
| } |
| } |
| |
| void DownloadBubbleUpdateService::CacheManager::OnDownloadItemUpdated( |
| download::DownloadItem* item) { |
| bool cache_was_at_max = IsDownloadItemCacheAtMax(); |
| bool removed_item = RemoveDownloadItemFromCache(item); |
| bool added_back_at_end = MaybeAddDownloadItemToCache( |
| item, /*is_new=*/false, |
| /*maybe_add_alert=*/update_service_->download_history_loaded()); |
| if (cache_was_at_max && removed_item && added_back_at_end) { |
| CHECK_EQ(download_items_.size(), GetNumItemsToCache()); |
| const ItemSortKey& last_key = |
| std::prev(GetLastIter(download_items_))->first; |
| update_service_->StartBackfillDownloadItems(last_key); |
| } |
| } |
| |
| void DownloadBubbleUpdateService::OnDownloadRemoved( |
| content::DownloadManager* manager, |
| download::DownloadItem* item) { |
| if (IsShutDown()) { |
| return; |
| } |
| CHECK(download_item_notifier_ || original_download_item_notifier_); |
| if (!download_item_notifier_) { |
| return; |
| } |
| GetCacheForItem(item).OnDownloadItemRemoved(item); |
| |
| auto* web_app_data = DownloadItemWebAppData::Get(item); |
| for (Browser* browser : chrome::FindAllBrowsersWithProfile(profile_)) { |
| if (browser->window() && |
| browser->window()->GetDownloadBubbleUIController() && |
| BrowserMatchesWebAppData(browser, web_app_data)) { |
| browser->window()->GetDownloadBubbleUIController()->OnDownloadItemRemoved( |
| item); |
| } |
| } |
| } |
| |
| void DownloadBubbleUpdateService::CacheManager::OnDownloadItemRemoved( |
| download::DownloadItem* item) { |
| bool cache_was_at_max = IsDownloadItemCacheAtMax(); |
| if (RemoveDownloadItemFromCache(item) && cache_was_at_max) { |
| CHECK_EQ(download_items_.size(), GetNumItemsToCache() - 1); |
| const ItemSortKey& last_key = GetLastIter(download_items_)->first; |
| update_service_->StartBackfillDownloadItems(last_key); |
| } |
| } |
| |
| void DownloadBubbleUpdateService::OnManagerGoingDown( |
| content::DownloadManager* manager) { |
| CHECK(download_item_notifier_ || original_download_item_notifier_); |
| // Assume that the original manager (if this is an OTR profile) may or may not |
| // have shut down, but we still want to cease all operations when this |
| // profile's manager shuts down. |
| if (download_item_notifier_ && |
| (manager == download_item_notifier_->GetManager())) { |
| is_shut_down_ = true; |
| download_item_notifier_.reset(); |
| for (CacheManager* cache : GetAllCacheManagers()) { |
| cache->DropAllDownloadItems(); |
| } |
| } |
| } |
| |
| void DownloadBubbleUpdateService::OnItemsAdded( |
| const OfflineContentProvider::OfflineItemList& items) { |
| if (IsShutDown()) { |
| return; |
| } |
| if (!offline_items_initialized_) { |
| offline_item_callbacks_.push_back( |
| base::BindOnce(&DownloadBubbleUpdateService::OnItemsAdded, |
| weak_factory_.GetWeakPtr(), items)); |
| return; |
| } |
| for (const OfflineItem& item : items) { |
| main_cache_.MaybeAddOfflineItemToCache(item, /*is_new=*/true, |
| /*maybe_add_alert=*/true); |
| } |
| |
| for (Browser* browser : chrome::FindAllBrowsersWithProfile(profile_)) { |
| if (browser->window() && |
| browser->window()->GetDownloadBubbleUIController() && |
| !web_app::AppBrowserController::IsWebApp(browser)) { |
| browser->window()->GetDownloadBubbleUIController()->OnOfflineItemsAdded( |
| items); |
| } |
| } |
| } |
| |
| void DownloadBubbleUpdateService::OnItemRemoved(const ContentId& id) { |
| if (IsShutDown()) { |
| return; |
| } |
| if (!offline_items_initialized_) { |
| offline_item_callbacks_.push_back( |
| base::BindOnce(&DownloadBubbleUpdateService::OnItemRemoved, |
| weak_factory_.GetWeakPtr(), id)); |
| return; |
| } |
| main_cache_.OnOfflineItemRemoved(id); |
| |
| for (Browser* browser : chrome::FindAllBrowsersWithProfile(profile_)) { |
| if (browser->window() && |
| browser->window()->GetDownloadBubbleUIController() && |
| !web_app::AppBrowserController::IsWebApp(browser)) { |
| browser->window()->GetDownloadBubbleUIController()->OnOfflineItemRemoved( |
| id); |
| } |
| } |
| } |
| |
| void DownloadBubbleUpdateService::CacheManager::OnOfflineItemRemoved( |
| const ContentId& id) { |
| bool cache_was_at_max = IsOfflineItemCacheAtMax(); |
| if (RemoveOfflineItemFromCache(id) && cache_was_at_max) { |
| CHECK_EQ(offline_items_.size(), GetNumItemsToCache() - 1); |
| const ItemSortKey& last_key = GetLastIter(offline_items_)->first; |
| update_service_->StartBackfillOfflineItems(last_key); |
| } |
| } |
| |
| void DownloadBubbleUpdateService::OnItemUpdated( |
| const OfflineItem& item, |
| const std::optional<offline_items_collection::UpdateDelta>& update_delta) { |
| if (IsShutDown()) { |
| return; |
| } |
| if (!offline_items_initialized_) { |
| offline_item_callbacks_.push_back( |
| base::BindOnce(&DownloadBubbleUpdateService::OnItemUpdated, |
| weak_factory_.GetWeakPtr(), item, update_delta)); |
| return; |
| } |
| main_cache_.OnOfflineItemUpdated(item); |
| |
| for (Browser* browser : chrome::FindAllBrowsersWithProfile(profile_)) { |
| if (browser->window() && |
| browser->window()->GetDownloadBubbleUIController() && |
| !web_app::AppBrowserController::IsWebApp(browser)) { |
| browser->window()->GetDownloadBubbleUIController()->OnOfflineItemUpdated( |
| item); |
| } |
| } |
| } |
| |
| void DownloadBubbleUpdateService::CacheManager::OnOfflineItemUpdated( |
| const OfflineItem& item) { |
| bool cache_was_at_max = IsOfflineItemCacheAtMax(); |
| bool removed_item = RemoveOfflineItemFromCache(GetItemId(item)); |
| bool added_back_to_end = MaybeAddOfflineItemToCache(item, /*is_new=*/false, |
| /*maybe_add_alert=*/true); |
| if (cache_was_at_max && removed_item && added_back_to_end) { |
| CHECK_EQ(offline_items_.size(), GetNumItemsToCache()); |
| const ItemSortKey& last_key = std::prev(GetLastIter(offline_items_))->first; |
| update_service_->StartBackfillOfflineItems(last_key); |
| } |
| } |
| |
| void DownloadBubbleUpdateService::OnContentProviderGoingDown() { |
| is_shut_down_ = true; |
| main_cache_.DropAllOfflineItems(); |
| } |
| |
| void DownloadBubbleUpdateService::OnHistoryQueryComplete() { |
| download_history_loaded_ = true; |
| } |
| |
| void DownloadBubbleUpdateService::OnDownloadHistoryDestroyed() { |
| download_history_observation_.Reset(); |
| } |
| |
| bool DownloadBubbleUpdateService::CacheManager::MaybeAddDownloadItemToCache( |
| download::DownloadItem* item, |
| bool is_new, |
| bool maybe_add_alert) { |
| DownloadItemModel model( |
| item, std::make_unique<DownloadUIModel::BubbleStatusTextBuilder>()); |
| if (!ShouldIncludeModel(&model, GetCutoffTime())) { |
| return false; |
| } |
| if (is_new && model.ShouldNotifyUI()) { |
| model.SetActionedOn(false); |
| } |
| if (maybe_add_alert) { |
| // Garbage collect accessible alerts before we add another item because this |
| // can be called after a long time since the last alert activity. |
| accessible_alerts_.GarbageCollect(); |
| accessible_alerts_.MaybeAddAccessibleAlert( |
| model.GetContentId(), GetAccessibleAlertForModel(model)); |
| } |
| return AddItemToCacheImpl(item, download_items_, download_items_iter_map_); |
| } |
| |
| bool DownloadBubbleUpdateService::CacheManager::MaybeAddOfflineItemToCache( |
| const OfflineItem& item, |
| bool is_new, |
| bool maybe_add_alert) { |
| CHECK(update_service_->IsMainCache(*this)); |
| if (update_service_->IsProfileOtr() != item.is_off_the_record) { |
| return false; |
| } |
| if (OfflineItemUtils::IsDownload(item.id)) { |
| return false; |
| } |
| if (item.state == offline_items_collection::OfflineItemState::CANCELLED) { |
| return false; |
| } |
| if (item.id.name_space == ContentIndexProviderImpl::kProviderNamespace) { |
| return false; |
| } |
| |
| OfflineItemModel model( |
| update_service_->GetOfflineManager(), item, |
| std::make_unique<DownloadUIModel::BubbleStatusTextBuilder>()); |
| if (!ShouldIncludeModel(&model, GetCutoffTime())) { |
| return false; |
| } |
| if (is_new && model.ShouldNotifyUI()) { |
| model.SetActionedOn(false); |
| } |
| if (maybe_add_alert) { |
| // Garbage collect accessible alerts before we add another item because this |
| // can be called after a long time since the last alert activity. |
| accessible_alerts_.GarbageCollect(); |
| accessible_alerts_.MaybeAddAccessibleAlert( |
| model.GetContentId(), GetAccessibleAlertForModel(model)); |
| } |
| |
| return AddItemToCacheImpl(item, offline_items_, offline_items_iter_map_); |
| } |
| |
| template <typename Id, typename Item> |
| bool DownloadBubbleUpdateService::CacheManager::AddItemToCacheImpl( |
| Item item, |
| SortedItems<Item>& cache, |
| IterMap<Id, Item>& iter_map) { |
| // This check duplicates part of the ShouldIncludeModel() check, but is still |
| // needed because we don't always call that before this function. |
| if (GetItemStartTime(item) < GetCutoffTime()) { |
| return false; |
| } |
| Id id = GetItemId(item); |
| if (iter_map.contains(id)) { |
| return false; |
| } |
| ItemSortKey key = GetSortKey(item); |
| |
| if (cache.size() >= GetNumItemsToCache()) { |
| CHECK_EQ(cache.size(), GetNumItemsToCache()); |
| if (key > GetLastIter(cache)->first) { |
| return false; |
| } |
| } |
| |
| auto it = cache.insert(std::make_pair(std::move(key), item)); |
| iter_map.insert(std::make_pair(id, it)); |
| |
| while (cache.size() > GetNumItemsToCache()) { |
| CHECK(!cache.empty()); |
| CHECK_EQ(cache.size(), 1 + GetNumItemsToCache()); |
| auto to_remove = GetLastIter(cache); |
| const Id& id_to_remove = GetItemId(to_remove->second); |
| iter_map.erase(id_to_remove); |
| cache.erase(to_remove); |
| } |
| |
| UpdateDisplayInfo(id); |
| |
| CHECK(!cache.empty()); |
| auto last_it = GetLastIter(cache); |
| return GetItemId(last_it->second) == id; |
| } |
| |
| bool DownloadBubbleUpdateService::CacheManager::RemoveDownloadItemFromCache( |
| download::DownloadItem* item) { |
| return RemoveItemFromCacheImpl(GetItemId(item), download_items_, |
| download_items_iter_map_); |
| } |
| |
| bool DownloadBubbleUpdateService::CacheManager::RemoveOfflineItemFromCache( |
| const ContentId& id) { |
| CHECK(update_service_->IsMainCache(*this)); |
| return RemoveItemFromCacheImpl(id, offline_items_, offline_items_iter_map_); |
| } |
| |
| template <typename Id, typename Item> |
| bool DownloadBubbleUpdateService::CacheManager::RemoveItemFromCacheImpl( |
| const Id& id, |
| SortedItems<Item>& cache, |
| IterMap<Id, Item>& iter_map) { |
| auto iter_map_it = iter_map.find(id); |
| if (iter_map_it == iter_map.end()) { |
| return false; |
| } |
| |
| cache.erase(iter_map_it->second); |
| iter_map.erase(iter_map_it); |
| |
| UpdateDisplayInfo(id); |
| |
| CHECK(cache.size() < GetNumItemsToCache()); |
| return true; |
| } |
| |
| template <typename Id, typename Item> |
| SortedItems<Item>::iterator |
| DownloadBubbleUpdateService::CacheManager::RemoveItemFromCacheByIter( |
| SortedItems<Item>::iterator iter, |
| SortedItems<Item>& cache, |
| IterMap<Id, Item>& iter_map) { |
| CHECK(iter != cache.end()); |
| auto next_iter = std::next(iter); |
| Id id = GetItemId(iter->second); |
| iter_map.erase(id); |
| cache.erase(iter); |
| |
| UpdateDisplayInfo(id); |
| |
| return next_iter; |
| } |
| |
| void DownloadBubbleUpdateService::StartBackfillDownloadItems( |
| const ItemSortKey& last_key) { |
| if (IsShutDown()) { |
| return; |
| } |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&DownloadBubbleUpdateService::BackfillDownloadItems, |
| weak_factory_.GetWeakPtr(), last_key)); |
| } |
| |
| void DownloadBubbleUpdateService::BackfillDownloadItems( |
| const ItemSortKey& last_key) { |
| if (IsShutDown()) { |
| return; |
| } |
| for (download::DownloadItem* item : GetAllDownloadItems()) { |
| if (GetSortKey(item) < last_key) { |
| continue; |
| } |
| GetCacheForItem(item).MaybeAddDownloadItemToCache( |
| item, /*is_new=*/false, /*maybe_add_alert=*/false); |
| } |
| } |
| |
| void DownloadBubbleUpdateService::StartBackfillOfflineItems( |
| const ItemSortKey& last_key) { |
| if (IsShutDown()) { |
| return; |
| } |
| offline_items_collection::OfflineContentProvider* provider = |
| OfflineContentAggregatorFactory::GetForKey(profile_->GetProfileKey()); |
| provider->GetAllItems( |
| base::BindOnce(&DownloadBubbleUpdateService::BackfillOfflineItems, |
| weak_factory_.GetWeakPtr(), last_key)); |
| } |
| |
| void DownloadBubbleUpdateService::BackfillOfflineItems( |
| const ItemSortKey& last_key, |
| const std::vector<OfflineItem>& all_items) { |
| if (IsShutDown()) { |
| return; |
| } |
| for (const OfflineItem& item : all_items) { |
| if (GetSortKey(item) < last_key) { |
| continue; |
| } |
| main_cache_.MaybeAddOfflineItemToCache(item, /*is_new=*/false, |
| /*maybe_add_alert=*/false); |
| } |
| } |
| |
| void DownloadBubbleUpdateService::CacheManager::DropAllDownloadItems() { |
| download_items_.clear(); |
| download_items_iter_map_.clear(); |
| } |
| |
| void DownloadBubbleUpdateService::InitializeDownloadItemsCache() { |
| CHECK(download_item_notifier_); |
| for (CacheManager* cache : GetAllCacheManagers()) { |
| cache->DropAllDownloadItems(); |
| } |
| for (download::DownloadItem* item : GetAllDownloadItems()) { |
| GetCacheForItem(item).MaybeAddDownloadItemToCache( |
| item, /*is_new=*/false, /*maybe_add_alert=*/false); |
| } |
| } |
| |
| void DownloadBubbleUpdateService::CacheManager::DropAllOfflineItems() { |
| CHECK(update_service_->IsMainCache(*this)); |
| offline_items_.clear(); |
| offline_items_iter_map_.clear(); |
| } |
| |
| void DownloadBubbleUpdateService::StartInitializeOfflineItemsCache() { |
| if (IsShutDown()) { |
| return; |
| } |
| if (offline_items_initialized_) { |
| return; |
| } |
| offline_items_collection::OfflineContentProvider* provider = |
| OfflineContentAggregatorFactory::GetForKey(profile_->GetProfileKey()); |
| provider->GetAllItems( |
| base::BindOnce(&DownloadBubbleUpdateService::InitializeOfflineItemsCache, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void DownloadBubbleUpdateService::InitializeOfflineItemsCache( |
| const std::vector<OfflineItem>& all_items) { |
| main_cache_.DropAllOfflineItems(); |
| for (const OfflineItem& item : all_items) { |
| main_cache_.MaybeAddOfflineItemToCache(item, /*is_new=*/false, |
| /*maybe_add_alert=*/false); |
| } |
| offline_items_initialized_ = true; |
| for (auto& callback : offline_item_callbacks_) { |
| std::move(callback).Run(); |
| } |
| offline_item_callbacks_.clear(); |
| } |
| |
| std::vector<raw_ptr<download::DownloadItem, VectorExperimental>> |
| DownloadBubbleUpdateService::GetAllDownloadItems() { |
| std::vector<raw_ptr<download::DownloadItem, VectorExperimental>> all_items; |
| if (download_item_notifier_) { |
| download_item_notifier_->GetManager()->GetAllDownloads(&all_items); |
| } |
| if (original_download_item_notifier_) { |
| original_download_item_notifier_->GetManager()->GetAllDownloads(&all_items); |
| } |
| return all_items; |
| } |
| |
| OfflineItemModelManager* DownloadBubbleUpdateService::GetOfflineManager() |
| const { |
| return OfflineItemModelManagerFactory::GetForBrowserContext(profile_); |
| } |
| |
| bool DownloadBubbleUpdateService::CacheManager::MaybeAddDownloadItemModel( |
| download::DownloadItem* item, |
| base::Time cutoff_time, |
| std::vector<DownloadUIModelPtr>& models) { |
| DownloadUIModelPtr model = DownloadItemModel::Wrap( |
| item, std::make_unique<DownloadUIModel::BubbleStatusTextBuilder>()); |
| return MaybeAddModel(std::move(model), cutoff_time, GetMaxNumItemsToShow(), |
| models); |
| } |
| |
| bool DownloadBubbleUpdateService::CacheManager::MaybeAddOfflineItemModel( |
| const offline_items_collection::OfflineItem& item, |
| base::Time cutoff_time, |
| std::vector<DownloadUIModelPtr>& models) { |
| DownloadUIModelPtr model = OfflineItemModel::Wrap( |
| update_service_->GetOfflineManager(), item, |
| std::make_unique<DownloadUIModel::BubbleStatusTextBuilder>()); |
| return MaybeAddModel(std::move(model), cutoff_time, GetMaxNumItemsToShow(), |
| models); |
| } |
| |
| void DownloadBubbleUpdateService::CacheManager::AppendBackfilledDownloadItems( |
| const ItemSortKey& last_key, |
| base::Time cutoff_time, |
| std::vector<DownloadUIModelPtr>& models) { |
| // This is not quite right because there might be a newly backfilled item |
| // whose key is equal to |last_key| that this would then skip |
| // over (and we would not be able to detect/fix the omission, unless the item |
| // received an update later), but this should happen rarely enough (requires |
| // two download items with the exact same creation time) that we will not |
| // handle this case. |
| auto it = download_items_.upper_bound(last_key); |
| |
| while (it != download_items_.end()) { |
| if (!MaybeAddDownloadItemModel(it->second, cutoff_time, models)) { |
| it = RemoveItemFromCacheByIter(it, download_items_, |
| download_items_iter_map_); |
| } else { |
| ++it; |
| } |
| } |
| } |
| |
| bool DownloadBubbleUpdateService::IsMainCache( |
| const DownloadBubbleUpdateService::CacheManager& cache) const { |
| return &cache == &main_cache_; |
| } |
| |
| void DownloadBubbleUpdateService::OnEphemeralWarningExpired( |
| const std::string& guid) { |
| if (IsShutDown()) { |
| return; |
| } |
| CHECK(download_item_notifier_ || original_download_item_notifier_); |
| if (!download_item_notifier_) { |
| return; |
| } |
| content::DownloadManager* download_manager = GetDownloadManager(); |
| if (!download_manager) { |
| return; |
| } |
| |
| download::DownloadItem* item = download_manager->GetDownloadByGuid(guid); |
| // The item might be from the original profile. |
| if (!item && original_download_item_notifier_ && |
| original_download_item_notifier_->GetManager()) { |
| item = |
| original_download_item_notifier_->GetManager()->GetDownloadByGuid(guid); |
| } |
| if (!item) { |
| return; |
| } |
| |
| GetCacheForItem(item).UpdateDisplayInfo(guid); |
| |
| auto* web_app_data = DownloadItemWebAppData::Get(item); |
| for (Browser* browser : chrome::FindAllBrowsersWithProfile(profile_)) { |
| if (browser->window() && |
| browser->window()->GetDownloadBubbleUIController() && |
| BrowserMatchesWebAppData(browser, web_app_data)) { |
| browser->window()->GetDownloadBubbleUIController()->OnDownloadItemRemoved( |
| item); |
| } |
| } |
| } |
| |
| #if DCHECK_IS_ON() |
| void DownloadBubbleUpdateService::CacheManager::ConsistencyCheckCaches() const { |
| ConsistencyCheckImpl(download_items_, download_items_iter_map_); |
| ConsistencyCheckImpl(offline_items_, offline_items_iter_map_); |
| } |
| |
| template <typename Id, typename Item> |
| void DownloadBubbleUpdateService::CacheManager::ConsistencyCheckImpl( |
| const SortedItems<Item>& cache, |
| const IterMap<Id, Item>& iter_map) const { |
| DCHECK_EQ(cache.size(), iter_map.size()) |
| << "Cache size " << cache.size() << " does not match index size " |
| << iter_map.size() << "."; |
| DCHECK_LE(cache.size(), GetNumItemsToCache()) |
| << "Cache size " << cache.size() << " exceeds max size " |
| << GetNumItemsToCache() << "."; |
| for (auto it = cache.begin(); it != cache.end(); ++it) { |
| const ItemSortKey& key = it->first; |
| const Item& item = it->second; |
| // The state of the stored key and the current state of the item may not |
| // always match, if we haven't received the update notification yet. |
| DCHECK_EQ(key.start_time, GetSortKey(item).start_time) |
| << "Start time in key does not match item."; |
| const Id& id = GetItemId(item); |
| auto iter_map_it = iter_map.find(id); |
| DCHECK(iter_map_it != iter_map.end()) << "Item id not in index."; |
| DCHECK(iter_map_it->second == it) << "Index inconsistent."; |
| } |
| } |
| #endif // DCHECK_IS_ON() |