| // Copyright 2012 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 "chrome/browser/win/jumplist.h" |
| |
| #include "base/base_paths.h" |
| #include "base/bind.h" |
| #include "base/bind_helpers.h" |
| #include "base/command_line.h" |
| #include "base/containers/flat_set.h" |
| #include "base/files/file_util.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/path_service.h" |
| #include "base/sequenced_task_runner.h" |
| #include "base/single_thread_task_runner.h" |
| #include "base/stl_util.h" |
| #include "base/strings/string_piece.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task/post_task.h" |
| #include "base/task/task_traits.h" |
| #include "base/threading/thread.h" |
| #include "base/timer/elapsed_timer.h" |
| #include "base/trace_event/trace_event.h" |
| #include "chrome/browser/chrome_notification_types.h" |
| #include "chrome/browser/favicon/favicon_service_factory.h" |
| #include "chrome/browser/history/top_sites_factory.h" |
| #include "chrome/browser/metrics/jumplist_metrics_win.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/sessions/tab_restore_service_factory.h" |
| #include "chrome/browser/shell_integration_win.h" |
| #include "chrome/browser/win/jumplist_file_util.h" |
| #include "chrome/browser/win/jumplist_update_util.h" |
| #include "chrome/common/chrome_constants.h" |
| #include "chrome/common/chrome_icon_resources_win.h" |
| #include "chrome/common/chrome_switches.h" |
| #include "chrome/common/pref_names.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "chrome/install_static/install_util.h" |
| #include "components/favicon/core/favicon_service.h" |
| #include "components/history/core/browser/history_service.h" |
| #include "components/history/core/browser/top_sites.h" |
| #include "components/prefs/pref_change_registrar.h" |
| #include "components/sessions/core/session_types.h" |
| #include "components/strings/grit/components_strings.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/gfx/codec/png_codec.h" |
| #include "ui/gfx/favicon_size.h" |
| #include "ui/gfx/icon_util.h" |
| #include "ui/gfx/image/image.h" |
| #include "ui/gfx/image/image_family.h" |
| #include "ui/gfx/image/image_skia.h" |
| #include "ui/gfx/image/image_skia_rep.h" |
| #include "url/gurl.h" |
| |
| namespace { |
| |
| // The default maximum number of items to display in JumpList is 10. |
| // https://msdn.microsoft.com/library/windows/desktop/dd378398.aspx |
| // The "Most visited" and "Recently closed" category titles always take 2 slots. |
| // For the remaining 8 slots, we allocate 5 slots to "most-visited" items and 3 |
| // slots to "recently-closed" items, respectively. |
| constexpr size_t kMostVisitedItems = 5; |
| constexpr size_t kRecentlyClosedItems = 3; |
| |
| // The number of update notifications to skip to alleviate the machine when a |
| // previous update was too slow. |
| constexpr int kNotificationsToSkipUnderHeavyLoad = 2; |
| |
| // The delay before updating the JumpList for users who haven't used it in a |
| // session. A delay of 2000 ms is chosen to coalesce more updates when tabs are |
| // closed rapidly. |
| constexpr base::TimeDelta kLongDelayForUpdate = |
| base::TimeDelta::FromMilliseconds(2000); |
| |
| // The delay before updating the JumpList for users who have used it in a |
| // session. A delay of 500 ms is used to not only make the update happen almost |
| // immediately, but also prevent update storms when tabs are closed rapidly via |
| // Ctrl-W. |
| constexpr base::TimeDelta kShortDelayForUpdate = |
| base::TimeDelta::FromMilliseconds(500); |
| |
| // The maximum allowed time for JumpListUpdater::BeginUpdate. Updates taking |
| // longer than this are discarded to prevent bogging down slow machines. |
| constexpr base::TimeDelta kTimeOutForBeginUpdate = |
| base::TimeDelta::FromMilliseconds(500); |
| |
| // The maximum allowed time for adding most visited pages custom category via |
| // JumpListUpdater::AddCustomCategory. |
| constexpr base::TimeDelta kTimeOutForAddCustomCategory = |
| base::TimeDelta::FromMilliseconds(320); |
| |
| // The maximum allowed time for JumpListUpdater::CommitUpdate. |
| constexpr base::TimeDelta kTimeOutForCommitUpdate = |
| base::TimeDelta::FromMilliseconds(1000); |
| |
| // Appends the common switches to each shell link. |
| void AppendCommonSwitches(ShellLinkItem* shell_link) { |
| const char* kSwitchNames[] = { switches::kUserDataDir }; |
| const base::CommandLine& command_line = |
| *base::CommandLine::ForCurrentProcess(); |
| shell_link->GetCommandLine()->CopySwitchesFrom(command_line, kSwitchNames, |
| base::size(kSwitchNames)); |
| } |
| |
| // Creates a ShellLinkItem preloaded with common switches. |
| scoped_refptr<ShellLinkItem> CreateShellLink() { |
| auto link = base::MakeRefCounted<ShellLinkItem>(); |
| AppendCommonSwitches(link.get()); |
| return link; |
| } |
| |
| // Creates a temporary icon file to be shown in JumpList. |
| bool CreateIconFile(const gfx::ImageSkia& image_skia, |
| const base::FilePath& icon_dir, |
| base::FilePath* icon_path) { |
| // Retrieve the path to a temporary file. |
| // We don't have to care about the extension of this temporary file because |
| // JumpList does not care about it. |
| base::FilePath path; |
| if (!base::CreateTemporaryFileInDir(icon_dir, &path)) |
| return false; |
| |
| // Create an icon file from the favicon attached to the given |page|, and |
| // save it as the temporary file. |
| gfx::ImageFamily image_family; |
| if (!image_skia.isNull()) { |
| std::vector<float> supported_scales = image_skia.GetSupportedScales(); |
| for (auto& scale : supported_scales) { |
| gfx::ImageSkiaRep image_skia_rep = image_skia.GetRepresentation(scale); |
| if (!image_skia_rep.is_null()) { |
| image_family.Add( |
| gfx::Image::CreateFrom1xBitmap(image_skia_rep.GetBitmap())); |
| } |
| } |
| } |
| |
| if (!IconUtil::CreateIconFileFromImageFamily(image_family, path, |
| IconUtil::NORMAL_WRITE)) { |
| // Delete the file created by CreateTemporaryFileInDir as it won't be used. |
| base::DeleteFile(path, false); |
| return false; |
| } |
| |
| // Add this icon file to the list and return its absolute path. |
| // The IShellLink::SetIcon() function needs the absolute path to an icon. |
| *icon_path = path; |
| return true; |
| } |
| |
| // Updates the "Tasks" category of the JumpList. |
| bool UpdateTaskCategory( |
| JumpListUpdater* jumplist_updater, |
| IncognitoModePrefs::Availability incognito_availability) { |
| base::FilePath chrome_path; |
| if (!base::PathService::Get(base::FILE_EXE, &chrome_path)) |
| return false; |
| |
| int icon_index = install_static::GetIconResourceIndex(); |
| |
| ShellLinkItemList items; |
| |
| // Create an IShellLink object which launches Chrome, and add it to the |
| // collection. We use our application icon as the icon for this item. |
| // We remove '&' characters from this string so we can share it with our |
| // system menu. |
| if (incognito_availability != IncognitoModePrefs::FORCED) { |
| scoped_refptr<ShellLinkItem> chrome = CreateShellLink(); |
| base::string16 chrome_title = l10n_util::GetStringUTF16(IDS_NEW_WINDOW); |
| base::ReplaceSubstringsAfterOffset( |
| &chrome_title, 0, L"&", base::StringPiece16()); |
| chrome->set_title(chrome_title); |
| chrome->set_icon(chrome_path.value(), icon_index); |
| items.push_back(chrome); |
| } |
| |
| // Create an IShellLink object which launches Chrome in incognito mode, and |
| // add it to the collection. |
| if (incognito_availability != IncognitoModePrefs::DISABLED) { |
| scoped_refptr<ShellLinkItem> incognito = CreateShellLink(); |
| incognito->GetCommandLine()->AppendSwitch(switches::kIncognito); |
| base::string16 incognito_title = |
| l10n_util::GetStringUTF16(IDS_NEW_INCOGNITO_WINDOW); |
| base::ReplaceSubstringsAfterOffset( |
| &incognito_title, 0, L"&", base::StringPiece16()); |
| incognito->set_title(incognito_title); |
| incognito->set_icon(chrome_path.value(), icon_resources::kIncognitoIndex); |
| items.push_back(incognito); |
| } |
| |
| return jumplist_updater->AddTasks(items); |
| } |
| |
| // Returns the full path of the JumpListIcons[|suffix|] directory in |
| // |profile_dir|. |
| base::FilePath GenerateJumplistIconDirName( |
| const base::FilePath& profile_dir, |
| const base::FilePath::StringPieceType& suffix) { |
| base::FilePath::StringType dir_name(chrome::kJumpListIconDirname); |
| suffix.AppendToString(&dir_name); |
| return profile_dir.Append(dir_name); |
| } |
| |
| } // namespace |
| |
| JumpList::UpdateTransaction::UpdateTransaction() {} |
| |
| JumpList::UpdateTransaction::~UpdateTransaction() {} |
| |
| // static |
| bool JumpList::Enabled() { |
| return JumpListUpdater::IsEnabled(); |
| } |
| |
| void JumpList::Shutdown() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| Terminate(); |
| } |
| |
| JumpList::JumpList(Profile* profile) |
| : profile_(profile), |
| update_jumplist_task_runner_(base::CreateCOMSTATaskRunnerWithTraits( |
| {base::MayBlock(), base::TaskPriority::USER_VISIBLE, |
| base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN})), |
| delete_jumplisticons_task_runner_( |
| base::CreateSequencedTaskRunnerWithTraits( |
| {base::MayBlock(), base::TaskPriority::BEST_EFFORT, |
| base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN})), |
| weak_ptr_factory_(this) { |
| DCHECK(Enabled()); |
| // To update JumpList when a tab is added or removed, we add this object to |
| // the observer list of the TabRestoreService class. |
| // When we add this object to the observer list, we save the pointer to this |
| // TabRestoreService object. This pointer is used when we remove this object |
| // from the observer list. |
| sessions::TabRestoreService* tab_restore_service = |
| TabRestoreServiceFactory::GetForProfile(profile_); |
| if (!tab_restore_service) |
| return; |
| |
| app_id_ = |
| shell_integration::win::GetChromiumModelIdForProfile(profile_->GetPath()); |
| |
| // Register as TopSitesObserver so that we can update ourselves when the |
| // TopSites changes. TopSites updates itself after a delay. This is especially |
| // noticable when your profile is empty. |
| scoped_refptr<history::TopSites> top_sites = |
| TopSitesFactory::GetForProfile(profile_); |
| if (top_sites) |
| top_sites->AddObserver(this); |
| |
| // Register as TabRestoreServiceObserver so that we can update ourselves when |
| // recently closed tabs have changes. |
| tab_restore_service->AddObserver(this); |
| |
| // kIncognitoModeAvailability is monitored for changes on Incognito mode. |
| pref_change_registrar_.reset(new PrefChangeRegistrar); |
| pref_change_registrar_->Init(profile_->GetPrefs()); |
| // base::Unretained is safe since |this| is guaranteed to outlive |
| // pref_change_registrar_. |
| pref_change_registrar_->Add( |
| prefs::kIncognitoModeAvailability, |
| base::Bind(&JumpList::OnIncognitoAvailabilityChanged, |
| base::Unretained(this))); |
| } |
| |
| JumpList::~JumpList() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| Terminate(); |
| } |
| |
| void JumpList::TopSitesLoaded(history::TopSites* top_sites) {} |
| |
| void JumpList::TopSitesChanged(history::TopSites* top_sites, |
| ChangeReason change_reason) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| top_sites_has_pending_notification_ = true; |
| |
| // Postpone handling this notification until a pending update completes. |
| if (update_in_progress_) |
| return; |
| |
| // If we have a pending favicon request, cancel it here as it's out of date. |
| CancelPendingUpdate(); |
| |
| // When the first tab is closed in one session, it doesn't trigger an update |
| // but a TopSites sync. This sync will trigger an update for both mostly |
| // visited and recently closed categories. We don't delay this TopSites sync. |
| if (has_topsites_sync) |
| InitializeTimerForUpdate(); |
| else |
| ProcessNotifications(); |
| } |
| |
| void JumpList::TabRestoreServiceChanged(sessions::TabRestoreService* service) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| tab_restore_has_pending_notification_ = true; |
| |
| // Postpone handling this notification until a pending update completes. |
| if (update_in_progress_) |
| return; |
| |
| // if we have a pending favicon request, cancel it here as it's out of date. |
| CancelPendingUpdate(); |
| |
| // Initialize the one-shot timer to update the JumpList in a while. |
| InitializeTimerForUpdate(); |
| } |
| |
| void JumpList::TabRestoreServiceDestroyed( |
| sessions::TabRestoreService* service) {} |
| |
| void JumpList::OnIncognitoAvailabilityChanged() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (icon_urls_.empty()) |
| PostRunUpdate(); |
| } |
| |
| void JumpList::InitializeTimerForUpdate() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (timer_.IsRunning()) { |
| timer_.Reset(); |
| } else { |
| // base::Unretained is safe since |this| is guaranteed to outlive timer_. |
| timer_.Start( |
| FROM_HERE, |
| profile_->GetUserData(chrome::kJumpListIconDirname) |
| ? kShortDelayForUpdate |
| : kLongDelayForUpdate, |
| base::Bind(&JumpList::ProcessNotifications, base::Unretained(this))); |
| } |
| } |
| |
| void JumpList::ProcessNotifications() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (updates_to_skip_ > 0) { |
| --updates_to_skip_; |
| return; |
| } |
| |
| // Retrieve the recently closed URLs synchronously. |
| if (tab_restore_has_pending_notification_) { |
| tab_restore_has_pending_notification_ = false; |
| ProcessTabRestoreServiceNotification(); |
| |
| // Force a TopSite history sync when closing a first tab in one session. |
| if (!has_tab_closed_) { |
| has_tab_closed_ = true; |
| scoped_refptr<history::TopSites> top_sites = |
| TopSitesFactory::GetForProfile(profile_); |
| if (top_sites) { |
| top_sites->SyncWithHistory(); |
| return; |
| } |
| } |
| } |
| |
| // If TopSites has updates, retrieve the URLs asynchronously, and on its |
| // completion, trigger favicon loading. |
| // Otherwise, call StartLoadingFavicon directly to start favicon loading. |
| if (top_sites_has_pending_notification_) |
| ProcessTopSitesNotification(); |
| else |
| StartLoadingFavicon(); |
| } |
| |
| void JumpList::ProcessTopSitesNotification() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // Opening the first tab in one session triggers a TopSite history sync. |
| // Delay this sync till the first tab is closed to allow the "recently closed" |
| // category from last session to stay longer. All previous pending |
| // notifications from TopSites are ignored. |
| if (!has_tab_closed_) { |
| top_sites_has_pending_notification_ = false; |
| return; |
| } |
| |
| has_topsites_sync = true; |
| |
| scoped_refptr<history::TopSites> top_sites = |
| TopSitesFactory::GetForProfile(profile_); |
| if (top_sites) { |
| top_sites->GetMostVisitedURLs(base::Bind( |
| &JumpList::OnMostVisitedURLsAvailable, weak_ptr_factory_.GetWeakPtr())); |
| } |
| } |
| |
| void JumpList::ProcessTabRestoreServiceNotification() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // Create a list of ShellLinkItems from the "Recently Closed" pages. |
| // As noted above, we create a ShellLinkItem objects with the following |
| // parameters. |
| // * arguments |
| // The last URL of the tab object. |
| // * title |
| // The title of the last URL. |
| // * icon |
| // An empty string. This value is to be updated in OnFaviconDataAvailable(). |
| |
| sessions::TabRestoreService* tab_restore_service = |
| TabRestoreServiceFactory::GetForProfile(profile_); |
| |
| recently_closed_pages_.clear(); |
| |
| for (const auto& entry : tab_restore_service->entries()) { |
| if (recently_closed_pages_.size() >= kRecentlyClosedItems) |
| break; |
| switch (entry->type) { |
| case sessions::TabRestoreService::TAB: |
| AddTab(static_cast<const sessions::TabRestoreService::Tab&>(*entry), |
| kRecentlyClosedItems); |
| break; |
| case sessions::TabRestoreService::WINDOW: |
| AddWindow( |
| static_cast<const sessions::TabRestoreService::Window&>(*entry), |
| kRecentlyClosedItems); |
| break; |
| } |
| } |
| |
| recently_closed_should_update_ = true; |
| } |
| |
| void JumpList::OnMostVisitedURLsAvailable( |
| const history::MostVisitedURLList& urls) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| top_sites_has_pending_notification_ = false; |
| |
| // There is no need to update the JumpList if the top most visited sites in |
| // display have not changed. |
| if (MostVisitedItemsUnchanged(most_visited_pages_, urls, kMostVisitedItems)) |
| return; |
| |
| most_visited_pages_.clear(); |
| |
| const size_t num_items = std::min(urls.size(), kMostVisitedItems); |
| for (size_t i = 0; i < num_items; ++i) { |
| const history::MostVisitedURL& url = urls[i]; |
| scoped_refptr<ShellLinkItem> link = CreateShellLink(); |
| std::string url_string = url.url.spec(); |
| base::string16 url_string_wide = base::UTF8ToUTF16(url_string); |
| link->GetCommandLine()->AppendArgNative(url_string_wide); |
| link->GetCommandLine()->AppendSwitchASCII(switches::kWinJumplistAction, |
| jumplist::kMostVisitedCategory); |
| link->set_title(!url.title.empty() ? url.title : url_string_wide); |
| link->set_url(url_string); |
| most_visited_pages_.push_back(link); |
| if (most_visited_icons_.find(url_string) == most_visited_icons_.end()) |
| icon_urls_.emplace_back(std::move(url_string), std::move(link)); |
| } |
| |
| most_visited_should_update_ = true; |
| |
| // Send a query that retrieves the first favicon. |
| StartLoadingFavicon(); |
| } |
| |
| bool JumpList::AddTab(const sessions::TabRestoreService::Tab& tab, |
| size_t max_items) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // This code adds the URL and the title strings of the given tab to the |
| // JumpList variables. |
| if (recently_closed_pages_.size() >= max_items) |
| return false; |
| |
| scoped_refptr<ShellLinkItem> link = CreateShellLink(); |
| const sessions::SerializedNavigationEntry& current_navigation = |
| tab.navigations.at(tab.current_navigation_index); |
| std::string url = current_navigation.virtual_url().spec(); |
| link->GetCommandLine()->AppendArgNative(base::UTF8ToUTF16(url)); |
| link->GetCommandLine()->AppendSwitchASCII(switches::kWinJumplistAction, |
| jumplist::kRecentlyClosedCategory); |
| link->set_title(current_navigation.title()); |
| link->set_url(url); |
| recently_closed_pages_.push_back(link); |
| if (recently_closed_icons_.find(url) == recently_closed_icons_.end()) |
| icon_urls_.emplace_back(std::move(url), std::move(link)); |
| |
| return true; |
| } |
| |
| void JumpList::AddWindow(const sessions::TabRestoreService::Window& window, |
| size_t max_items) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(!window.tabs.empty()); |
| |
| for (const auto& tab : window.tabs) { |
| if (!AddTab(*tab, max_items)) |
| return; |
| } |
| } |
| |
| void JumpList::StartLoadingFavicon() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (icon_urls_.empty()) { |
| // No more favicons are needed by the application JumpList. Schedule a |
| // RunUpdateJumpList call. |
| PostRunUpdate(); |
| return; |
| } |
| |
| // Ask FaviconService if it has a favicon of a URL. |
| // When FaviconService has one, it will call OnFaviconDataAvailable(). |
| favicon::FaviconService* favicon_service = |
| FaviconServiceFactory::GetForProfile(profile_, |
| ServiceAccessType::EXPLICIT_ACCESS); |
| // base::Unretained is safe since |this| is guaranteed to outlive |
| // cancelable_task_tracker_. |
| task_id_ = favicon_service->GetFaviconImageForPageURL( |
| GURL(icon_urls_.front().first), |
| base::Bind(&JumpList::OnFaviconDataAvailable, base::Unretained(this)), |
| &cancelable_task_tracker_); |
| } |
| |
| void JumpList::OnFaviconDataAvailable( |
| const favicon_base::FaviconImageResult& image_result) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // If there is currently a favicon request in progress, it is now outdated, |
| // as we have received another, so nullify the handle from the old request. |
| task_id_ = base::CancelableTaskTracker::kBadTaskId; |
| |
| // Attach the received data to the ShellLinkItem object. This data will be |
| // decoded by the RunUpdateJumpList method. |
| if (!icon_urls_.empty()) { |
| if (!image_result.image.IsEmpty() && icon_urls_.front().second.get()) { |
| gfx::ImageSkia image_skia = image_result.image.AsImageSkia(); |
| image_skia.EnsureRepsForSupportedScales(); |
| std::unique_ptr<gfx::ImageSkia> deep_copy(image_skia.DeepCopy()); |
| icon_urls_.front().second->set_icon_image(*deep_copy); |
| } |
| icon_urls_.pop_front(); |
| } |
| |
| // Check whether we need to load more favicons. |
| StartLoadingFavicon(); |
| } |
| |
| void JumpList::PostRunUpdate() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| TRACE_EVENT0("browser", "JumpList::PostRunUpdate"); |
| |
| update_in_progress_ = true; |
| |
| base::FilePath profile_dir = profile_->GetPath(); |
| |
| // Check if incognito windows (or normal windows) are disabled by policy. |
| IncognitoModePrefs::Availability incognito_availability = |
| IncognitoModePrefs::GetAvailability(profile_->GetPrefs()); |
| |
| auto update_transaction = std::make_unique<UpdateTransaction>(); |
| if (most_visited_should_update_) |
| update_transaction->most_visited_icons = std::move(most_visited_icons_); |
| if (recently_closed_should_update_) { |
| update_transaction->recently_closed_icons = |
| std::move(recently_closed_icons_); |
| } |
| |
| // Parameter evaluation order is unspecified in C++. Do the first bind and |
| // then move it into PostTaskAndReply to ensure the pointer value is obtained |
| // before base::Passed() is called. |
| auto run_update = |
| base::Bind(&JumpList::RunUpdateJumpList, app_id_, profile_dir, |
| most_visited_pages_, recently_closed_pages_, |
| most_visited_should_update_, recently_closed_should_update_, |
| incognito_availability, update_transaction.get()); |
| |
| // Post a task to update the JumpList, which consists of 1) create new icons, |
| // 2) notify the OS, 3) delete old icons. |
| if (!update_jumplist_task_runner_->PostTaskAndReply( |
| FROM_HERE, std::move(run_update), |
| base::Bind(&JumpList::OnRunUpdateCompletion, |
| weak_ptr_factory_.GetWeakPtr(), |
| base::Passed(std::move(update_transaction))))) { |
| OnRunUpdateCompletion(std::make_unique<UpdateTransaction>()); |
| } |
| } |
| |
| void JumpList::OnRunUpdateCompletion( |
| std::unique_ptr<UpdateTransaction> update_transaction) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // Update JumpList member variables based on the results from the update run |
| // just finished. |
| if (update_transaction->update_timeout) |
| updates_to_skip_ = kNotificationsToSkipUnderHeavyLoad; |
| |
| if (update_transaction->update_success) { |
| if (most_visited_should_update_) { |
| most_visited_icons_ = std::move(update_transaction->most_visited_icons); |
| most_visited_should_update_ = false; |
| } |
| if (recently_closed_should_update_) { |
| recently_closed_icons_ = |
| std::move(update_transaction->recently_closed_icons); |
| recently_closed_should_update_ = false; |
| } |
| } |
| |
| update_in_progress_ = false; |
| |
| // If there is any new notification during the update run just finished, start |
| // another JumpList update. |
| // Otherwise, post tasks to delete the JumpListIcons and JumpListIconsOld |
| // folders as they are no longer needed. Now we have the |
| // JumpListIcons{MostVisited, RecentClosed} folders instead. |
| if (top_sites_has_pending_notification_ || |
| tab_restore_has_pending_notification_) { |
| InitializeTimerForUpdate(); |
| } else { |
| base::FilePath profile_dir = profile_->GetPath(); |
| base::FilePath icon_dir = |
| GenerateJumplistIconDirName(profile_dir, FILE_PATH_LITERAL("")); |
| delete_jumplisticons_task_runner_->PostTask( |
| FROM_HERE, |
| base::Bind(&DeleteDirectory, std::move(icon_dir), kFileDeleteLimit)); |
| |
| base::FilePath icon_dir_old = |
| GenerateJumplistIconDirName(profile_dir, FILE_PATH_LITERAL("Old")); |
| delete_jumplisticons_task_runner_->PostTask( |
| FROM_HERE, base::Bind(&DeleteDirectory, std::move(icon_dir_old), |
| kFileDeleteLimit)); |
| } |
| } |
| |
| void JumpList::CancelPendingUpdate() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // Cancel a pending most-visited URL fetch by invalidating the weak pointer. |
| weak_ptr_factory_.InvalidateWeakPtrs(); |
| |
| // Cancel a pending favicon loading by invalidating its task id. |
| if (task_id_ != base::CancelableTaskTracker::kBadTaskId) { |
| cancelable_task_tracker_.TryCancel(task_id_); |
| task_id_ = base::CancelableTaskTracker::kBadTaskId; |
| } |
| } |
| |
| void JumpList::Terminate() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| timer_.Stop(); |
| CancelPendingUpdate(); |
| update_in_progress_ = false; |
| if (profile_) { |
| sessions::TabRestoreService* tab_restore_service = |
| TabRestoreServiceFactory::GetForProfile(profile_); |
| if (tab_restore_service) |
| tab_restore_service->RemoveObserver(this); |
| scoped_refptr<history::TopSites> top_sites = |
| TopSitesFactory::GetForProfile(profile_); |
| if (top_sites) |
| top_sites->RemoveObserver(this); |
| pref_change_registrar_.reset(); |
| } |
| profile_ = nullptr; |
| } |
| |
| // static |
| void JumpList::RunUpdateJumpList( |
| const base::string16& app_id, |
| const base::FilePath& profile_dir, |
| const ShellLinkItemList& most_visited_pages, |
| const ShellLinkItemList& recently_closed_pages, |
| bool most_visited_should_update, |
| bool recently_closed_should_update, |
| IncognitoModePrefs::Availability incognito_availability, |
| UpdateTransaction* update_transaction) { |
| DCHECK(update_transaction); |
| |
| base::FilePath most_visited_icon_dir = GenerateJumplistIconDirName( |
| profile_dir, FILE_PATH_LITERAL("MostVisited")); |
| base::FilePath recently_closed_icon_dir = GenerateJumplistIconDirName( |
| profile_dir, FILE_PATH_LITERAL("RecentClosed")); |
| |
| CreateNewJumpListAndNotifyOS( |
| app_id, most_visited_icon_dir, recently_closed_icon_dir, |
| most_visited_pages, recently_closed_pages, most_visited_should_update, |
| recently_closed_should_update, incognito_availability, |
| update_transaction); |
| |
| // Delete any obsolete icon files. |
| if (most_visited_should_update) { |
| DeleteIconFiles(most_visited_icon_dir, |
| update_transaction->most_visited_icons); |
| } |
| if (recently_closed_should_update) { |
| DeleteIconFiles(recently_closed_icon_dir, |
| update_transaction->recently_closed_icons); |
| } |
| } |
| |
| // static |
| void JumpList::CreateNewJumpListAndNotifyOS( |
| const base::string16& app_id, |
| const base::FilePath& most_visited_icon_dir, |
| const base::FilePath& recently_closed_icon_dir, |
| const ShellLinkItemList& most_visited_pages, |
| const ShellLinkItemList& recently_closed_pages, |
| bool most_visited_should_update, |
| bool recently_closed_should_update, |
| IncognitoModePrefs::Availability incognito_availability, |
| UpdateTransaction* update_transaction) { |
| DCHECK(update_transaction); |
| |
| JumpListUpdater jumplist_updater(app_id); |
| |
| base::ElapsedTimer begin_update_timer; |
| |
| bool begin_success = jumplist_updater.BeginUpdate(); |
| |
| // If JumpListUpdater::BeginUpdate takes longer than the maximum allowed time, |
| // abort the current update as it's very likely the following steps will also |
| // take a long time, and skip the next |kNotificationsToSkipUnderHeavyLoad| |
| // update notifications. |
| if (begin_update_timer.Elapsed() >= kTimeOutForBeginUpdate) { |
| update_transaction->update_timeout = true; |
| return; |
| } |
| |
| if (!begin_success) |
| return; |
| |
| // Record the desired number of icons created in this JumpList update. |
| int icons_created = 0; |
| |
| URLIconCache most_visited_icons_next; |
| URLIconCache recently_closed_icons_next; |
| |
| // Update the icons for "Most Visisted" category of the JumpList if needed. |
| if (most_visited_should_update) { |
| icons_created += UpdateIconFiles( |
| most_visited_icon_dir, most_visited_pages, kMostVisitedItems, |
| &update_transaction->most_visited_icons, &most_visited_icons_next); |
| } |
| |
| // Update the icons for "Recently Closed" category of the JumpList if needed. |
| if (recently_closed_should_update) { |
| icons_created += UpdateIconFiles( |
| recently_closed_icon_dir, recently_closed_pages, kRecentlyClosedItems, |
| &update_transaction->recently_closed_icons, |
| &recently_closed_icons_next); |
| } |
| |
| base::ElapsedTimer add_custom_category_timer; |
| |
| // Update the "Most Visited" category of the JumpList if it exists. |
| // This update request is applied into the JumpList when we commit this |
| // transaction. |
| bool add_category_success = jumplist_updater.AddCustomCategory( |
| l10n_util::GetStringUTF16(IDS_NEW_TAB_MOST_VISITED), most_visited_pages, |
| kMostVisitedItems); |
| |
| // If AddCustomCategory takes longer than the maximum allowed time, abort the |
| // current update and skip the next |kNotificationsToSkipUnderHeavyLoad| |
| // update notifications. |
| // |
| // We only time adding custom category for most visited pages because |
| // 1. If processing the first category times out or fails, there is no need to |
| // process the second category. In this case, we are not able to time both |
| // categories. Then we need to select one category from the two. |
| // 2. Most visited category is selected because it always has 5 items except |
| // for a new Chrome user who has not closed 5 distinct websites yet. In |
| // comparison, the number of items in recently closed category is much less |
| // stable. It has 3 items only after an user closes 3 websites in one |
| // session. This means the runtime of AddCustomCategory API should be fixed |
| // for most visited category, but not for recently closed category. So a |
| // fixed timeout threshold is only valid for most visited category. |
| if (add_custom_category_timer.Elapsed() >= kTimeOutForAddCustomCategory) { |
| update_transaction->update_timeout = true; |
| return; |
| } |
| |
| if (!add_category_success) |
| return; |
| |
| // Update the "Recently Closed" category of the JumpList. |
| if (!jumplist_updater.AddCustomCategory( |
| l10n_util::GetStringUTF16(IDS_RECENTLY_CLOSED), recently_closed_pages, |
| kRecentlyClosedItems)) { |
| return; |
| } |
| |
| // Update the "Tasks" category of the JumpList. |
| if (!UpdateTaskCategory(&jumplist_updater, incognito_availability)) |
| return; |
| |
| base::ElapsedTimer commit_update_timer; |
| |
| // Commit this transaction and send the updated JumpList to Windows. |
| bool commit_success = jumplist_updater.CommitUpdate(); |
| |
| // If CommitUpdate call takes longer than the maximum allowed time, skip the |
| // next |kNotificationsToSkipUnderHeavyLoad| update notifications. |
| if (commit_update_timer.Elapsed() >= kTimeOutForCommitUpdate) |
| update_transaction->update_timeout = true; |
| |
| if (commit_success) { |
| update_transaction->update_success = true; |
| |
| // The move assignments below ensure update_transaction always has the icons |
| // to keep. |
| if (most_visited_should_update) { |
| update_transaction->most_visited_icons = |
| std::move(most_visited_icons_next); |
| } |
| if (recently_closed_should_update) { |
| update_transaction->recently_closed_icons = |
| std::move(recently_closed_icons_next); |
| } |
| } |
| } |
| |
| // static |
| int JumpList::UpdateIconFiles(const base::FilePath& icon_dir, |
| const ShellLinkItemList& item_list, |
| size_t max_items, |
| URLIconCache* icon_cur, |
| URLIconCache* icon_next) { |
| DCHECK(icon_cur); |
| DCHECK(icon_next); |
| |
| // Clear the JumpList icon folder at |icon_dir| and the caches when |
| // 1) |icon_cur| is empty. This happens when "Most visited" or "Recently |
| // closed" category updates for the 1st time after Chrome is launched. |
| // 2) The number of icons in |icon_dir| has exceeded the limit. |
| if (icon_cur->empty() || FilesExceedLimitInDir(icon_dir, max_items * 2)) { |
| DeleteDirectoryContent(icon_dir, kFileDeleteLimit); |
| icon_cur->clear(); |
| icon_next->clear(); |
| // Create new icons only when the directory exists and is empty. |
| if (!base::CreateDirectory(icon_dir) || !base::IsDirectoryEmpty(icon_dir)) |
| return 0; |
| } else if (!base::CreateDirectory(icon_dir)) { |
| return 0; |
| } |
| |
| return CreateIconFiles(icon_dir, item_list, max_items, *icon_cur, icon_next); |
| } |
| |
| // static |
| int JumpList::CreateIconFiles(const base::FilePath& icon_dir, |
| const ShellLinkItemList& item_list, |
| size_t max_items, |
| const URLIconCache& icon_cur, |
| URLIconCache* icon_next) { |
| DCHECK(icon_next); |
| |
| int icons_created = 0; |
| |
| // Reuse icons for urls that already present in the current JumpList. |
| for (auto iter = item_list.begin(); iter != item_list.end() && max_items > 0; |
| ++iter, --max_items) { |
| ShellLinkItem* item = iter->get(); |
| auto cache_iter = icon_cur.find(item->url()); |
| if (cache_iter != icon_cur.end()) { |
| item->set_icon(cache_iter->second.value(), 0); |
| (*icon_next)[item->url()] = cache_iter->second; |
| } else { |
| base::FilePath icon_path; |
| if (CreateIconFile(item->icon_image(), icon_dir, &icon_path)) { |
| ++icons_created; |
| item->set_icon(icon_path.value(), 0); |
| (*icon_next)[item->url()] = icon_path; |
| } |
| } |
| } |
| |
| return icons_created; |
| } |
| |
| // static |
| void JumpList::DeleteIconFiles(const base::FilePath& icon_dir, |
| const URLIconCache& icons_cache) { |
| // Put all cached icon file paths into a set. |
| base::flat_set<base::FilePath> cached_files; |
| cached_files.reserve(icons_cache.size()); |
| |
| for (const auto& url_path_pair : icons_cache) |
| cached_files.insert(url_path_pair.second); |
| |
| DeleteNonCachedFiles(icon_dir, cached_files); |
| } |