jumplist_win.cc revision 116680a4aac90f2aa7413d9095a592090648e557
1// Copyright (c) 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#include "chrome/browser/jumplist_win.h" 6 7#include "base/bind.h" 8#include "base/bind_helpers.h" 9#include "base/command_line.h" 10#include "base/file_util.h" 11#include "base/path_service.h" 12#include "base/strings/string_util.h" 13#include "base/strings/utf_string_conversions.h" 14#include "base/threading/thread.h" 15#include "chrome/browser/chrome_notification_types.h" 16#include "chrome/browser/favicon/favicon_service.h" 17#include "chrome/browser/favicon/favicon_service_factory.h" 18#include "chrome/browser/history/history_service.h" 19#include "chrome/browser/history/page_usage_data.h" 20#include "chrome/browser/history/top_sites.h" 21#include "chrome/browser/profiles/profile.h" 22#include "chrome/browser/sessions/session_types.h" 23#include "chrome/browser/sessions/tab_restore_service.h" 24#include "chrome/browser/sessions/tab_restore_service_factory.h" 25#include "chrome/browser/shell_integration.h" 26#include "chrome/common/chrome_constants.h" 27#include "chrome/common/chrome_switches.h" 28#include "chrome/common/url_constants.h" 29#include "components/favicon_base/favicon_types.h" 30#include "content/public/browser/browser_thread.h" 31#include "content/public/browser/notification_source.h" 32#include "grit/chromium_strings.h" 33#include "grit/generated_resources.h" 34#include "ui/base/l10n/l10n_util.h" 35#include "ui/gfx/codec/png_codec.h" 36#include "ui/gfx/favicon_size.h" 37#include "ui/gfx/icon_util.h" 38#include "ui/gfx/image/image_family.h" 39#include "url/gurl.h" 40 41using content::BrowserThread; 42 43namespace { 44 45// Append the common switches to each shell link. 46void AppendCommonSwitches(ShellLinkItem* shell_link) { 47 const char* kSwitchNames[] = { switches::kUserDataDir }; 48 const CommandLine& command_line = *CommandLine::ForCurrentProcess(); 49 shell_link->GetCommandLine()->CopySwitchesFrom(command_line, 50 kSwitchNames, 51 arraysize(kSwitchNames)); 52} 53 54// Create a ShellLinkItem preloaded with common switches. 55scoped_refptr<ShellLinkItem> CreateShellLink() { 56 scoped_refptr<ShellLinkItem> link(new ShellLinkItem); 57 AppendCommonSwitches(link.get()); 58 return link; 59} 60 61// Creates a temporary icon file to be shown in JumpList. 62bool CreateIconFile(const SkBitmap& bitmap, 63 const base::FilePath& icon_dir, 64 base::FilePath* icon_path) { 65 // Retrieve the path to a temporary file. 66 // We don't have to care about the extension of this temporary file because 67 // JumpList does not care about it. 68 base::FilePath path; 69 if (!base::CreateTemporaryFileInDir(icon_dir, &path)) 70 return false; 71 72 // Create an icon file from the favicon attached to the given |page|, and 73 // save it as the temporary file. 74 gfx::ImageFamily image_family; 75 image_family.Add(gfx::Image::CreateFrom1xBitmap(bitmap)); 76 if (!IconUtil::CreateIconFileFromImageFamily(image_family, path)) 77 return false; 78 79 // Add this icon file to the list and return its absolute path. 80 // The IShellLink::SetIcon() function needs the absolute path to an icon. 81 *icon_path = path; 82 return true; 83} 84 85// Updates the "Tasks" category of the JumpList. 86bool UpdateTaskCategory(JumpListUpdater* jumplist_updater) { 87 base::FilePath chrome_path; 88 if (!PathService::Get(base::FILE_EXE, &chrome_path)) 89 return false; 90 91 ShellLinkItemList items; 92 93 // Create an IShellLink object which launches Chrome, and add it to the 94 // collection. We use our application icon as the icon for this item. 95 // We remove '&' characters from this string so we can share it with our 96 // system menu. 97 scoped_refptr<ShellLinkItem> chrome = CreateShellLink(); 98 base::string16 chrome_title = l10n_util::GetStringUTF16(IDS_NEW_WINDOW); 99 ReplaceSubstringsAfterOffset(&chrome_title, 0, L"&", L""); 100 chrome->set_title(chrome_title); 101 chrome->set_icon(chrome_path.value(), 0); 102 items.push_back(chrome); 103 104 // Create an IShellLink object which launches Chrome in incognito mode, and 105 // add it to the collection. We use our application icon as the icon for 106 // this item. 107 scoped_refptr<ShellLinkItem> incognito = CreateShellLink(); 108 incognito->GetCommandLine()->AppendSwitch(switches::kIncognito); 109 base::string16 incognito_title = 110 l10n_util::GetStringUTF16(IDS_NEW_INCOGNITO_WINDOW); 111 ReplaceSubstringsAfterOffset(&incognito_title, 0, L"&", L""); 112 incognito->set_title(incognito_title); 113 incognito->set_icon(chrome_path.value(), 0); 114 items.push_back(incognito); 115 116 return jumplist_updater->AddTasks(items); 117} 118 119// Updates the application JumpList. 120bool UpdateJumpList(const wchar_t* app_id, 121 const ShellLinkItemList& most_visited_pages, 122 const ShellLinkItemList& recently_closed_pages) { 123 // JumpList is implemented only on Windows 7 or later. 124 // So, we should return now when this function is called on earlier versions 125 // of Windows. 126 if (!JumpListUpdater::IsEnabled()) 127 return true; 128 129 JumpListUpdater jumplist_updater(app_id); 130 if (!jumplist_updater.BeginUpdate()) 131 return false; 132 133 // We allocate 60% of the given JumpList slots to "most-visited" items 134 // and 40% to "recently-closed" items, respectively. 135 // Nevertheless, if there are not so many items in |recently_closed_pages|, 136 // we give the remaining slots to "most-visited" items. 137 const int kMostVisited = 60; 138 const int kRecentlyClosed = 40; 139 const int kTotal = kMostVisited + kRecentlyClosed; 140 size_t most_visited_items = 141 MulDiv(jumplist_updater.user_max_items(), kMostVisited, kTotal); 142 size_t recently_closed_items = 143 jumplist_updater.user_max_items() - most_visited_items; 144 if (recently_closed_pages.size() < recently_closed_items) { 145 most_visited_items += recently_closed_items - recently_closed_pages.size(); 146 recently_closed_items = recently_closed_pages.size(); 147 } 148 149 // Update the "Most Visited" category of the JumpList. 150 // This update request is applied into the JumpList when we commit this 151 // transaction. 152 if (!jumplist_updater.AddCustomCategory( 153 base::UTF16ToWide( 154 l10n_util::GetStringUTF16(IDS_NEW_TAB_MOST_VISITED)), 155 most_visited_pages, most_visited_items)) { 156 return false; 157 } 158 159 // Update the "Recently Closed" category of the JumpList. 160 if (!jumplist_updater.AddCustomCategory( 161 base::UTF16ToWide( 162 l10n_util::GetStringUTF16(IDS_NEW_TAB_RECENTLY_CLOSED)), 163 recently_closed_pages, recently_closed_items)) { 164 return false; 165 } 166 167 // Update the "Tasks" category of the JumpList. 168 if (!UpdateTaskCategory(&jumplist_updater)) 169 return false; 170 171 // Commit this transaction and send the updated JumpList to Windows. 172 if (!jumplist_updater.CommitUpdate()) 173 return false; 174 175 return true; 176} 177 178} // namespace 179 180JumpList::JumpList() 181 : weak_ptr_factory_(this), 182 profile_(NULL), 183 task_id_(base::CancelableTaskTracker::kBadTaskId) {} 184 185JumpList::~JumpList() { 186 Terminate(); 187} 188 189// static 190bool JumpList::Enabled() { 191 return JumpListUpdater::IsEnabled(); 192} 193 194bool JumpList::AddObserver(Profile* profile) { 195 // To update JumpList when a tab is added or removed, we add this object to 196 // the observer list of the TabRestoreService class. 197 // When we add this object to the observer list, we save the pointer to this 198 // TabRestoreService object. This pointer is used when we remove this object 199 // from the observer list. 200 if (!JumpListUpdater::IsEnabled() || !profile) 201 return false; 202 203 TabRestoreService* tab_restore_service = 204 TabRestoreServiceFactory::GetForProfile(profile); 205 if (!tab_restore_service) 206 return false; 207 208 app_id_ = ShellIntegration::GetChromiumModelIdForProfile(profile->GetPath()); 209 icon_dir_ = profile->GetPath().Append(chrome::kJumpListIconDirname); 210 profile_ = profile; 211 history::TopSites* top_sites = profile_->GetTopSites(); 212 if (top_sites) { 213 // TopSites updates itself after a delay. This is especially noticable when 214 // your profile is empty. Ask TopSites to update itself when jumplist is 215 // initialized. 216 top_sites->SyncWithHistory(); 217 registrar_.reset(new content::NotificationRegistrar); 218 // Register for notification when TopSites changes so that we can update 219 // ourself. 220 registrar_->Add(this, chrome::NOTIFICATION_TOP_SITES_CHANGED, 221 content::Source<history::TopSites>(top_sites)); 222 // Register for notification when profile is destroyed to ensure that all 223 // observers are detatched at that time. 224 registrar_->Add(this, chrome::NOTIFICATION_PROFILE_DESTROYED, 225 content::Source<Profile>(profile_)); 226 } 227 tab_restore_service->AddObserver(this); 228 return true; 229} 230 231void JumpList::Observe(int type, 232 const content::NotificationSource& source, 233 const content::NotificationDetails& details) { 234 switch (type) { 235 case chrome::NOTIFICATION_TOP_SITES_CHANGED: { 236 // Most visited urls changed, query again. 237 history::TopSites* top_sites = profile_->GetTopSites(); 238 if (top_sites) { 239 top_sites->GetMostVisitedURLs( 240 base::Bind(&JumpList::OnMostVisitedURLsAvailable, 241 weak_ptr_factory_.GetWeakPtr()), false); 242 } 243 break; 244 } 245 case chrome::NOTIFICATION_PROFILE_DESTROYED: { 246 // Profile was destroyed, do clean-up. 247 Terminate(); 248 break; 249 } 250 default: 251 NOTREACHED() << "Unexpected notification type."; 252 } 253} 254 255void JumpList::RemoveObserver() { 256 if (profile_) { 257 TabRestoreService* tab_restore_service = 258 TabRestoreServiceFactory::GetForProfile(profile_); 259 if (tab_restore_service) 260 tab_restore_service->RemoveObserver(this); 261 registrar_.reset(); 262 } 263 profile_ = NULL; 264} 265 266void JumpList::CancelPendingUpdate() { 267 if (task_id_ != base::CancelableTaskTracker::kBadTaskId) { 268 cancelable_task_tracker_.TryCancel(task_id_); 269 task_id_ = base::CancelableTaskTracker::kBadTaskId; 270 } 271} 272 273void JumpList::Terminate() { 274 CancelPendingUpdate(); 275 RemoveObserver(); 276} 277 278void JumpList::OnMostVisitedURLsAvailable( 279 const history::MostVisitedURLList& data) { 280 281 // If we have a pending favicon request, cancel it here (it is out of date). 282 CancelPendingUpdate(); 283 284 { 285 base::AutoLock auto_lock(list_lock_); 286 most_visited_pages_.clear(); 287 for (size_t i = 0; i < data.size(); i++) { 288 const history::MostVisitedURL& url = data[i]; 289 scoped_refptr<ShellLinkItem> link = CreateShellLink(); 290 std::string url_string = url.url.spec(); 291 std::wstring url_string_wide = base::UTF8ToWide(url_string); 292 link->GetCommandLine()->AppendArgNative(url_string_wide); 293 link->set_title(!url.title.empty()? url.title : url_string_wide); 294 most_visited_pages_.push_back(link); 295 icon_urls_.push_back(make_pair(url_string, link)); 296 } 297 } 298 299 // Send a query that retrieves the first favicon. 300 StartLoadingFavicon(); 301} 302 303void JumpList::TabRestoreServiceChanged(TabRestoreService* service) { 304 // if we have a pending handle request, cancel it here (it is out of date). 305 CancelPendingUpdate(); 306 307 // local list to pass to methods 308 ShellLinkItemList temp_list; 309 310 // Create a list of ShellLinkItems from the "Recently Closed" pages. 311 // As noted above, we create a ShellLinkItem objects with the following 312 // parameters. 313 // * arguments 314 // The last URL of the tab object. 315 // * title 316 // The title of the last URL. 317 // * icon 318 // An empty string. This value is to be updated in OnFaviconDataAvailable(). 319 // This code is copied from 320 // RecentlyClosedTabsHandler::TabRestoreServiceChanged() to emulate it. 321 const int kRecentlyClosedCount = 4; 322 TabRestoreService* tab_restore_service = 323 TabRestoreServiceFactory::GetForProfile(profile_); 324 const TabRestoreService::Entries& entries = tab_restore_service->entries(); 325 for (TabRestoreService::Entries::const_iterator it = entries.begin(); 326 it != entries.end(); ++it) { 327 const TabRestoreService::Entry* entry = *it; 328 if (entry->type == TabRestoreService::TAB) { 329 AddTab(static_cast<const TabRestoreService::Tab*>(entry), 330 &temp_list, kRecentlyClosedCount); 331 } else if (entry->type == TabRestoreService::WINDOW) { 332 AddWindow(static_cast<const TabRestoreService::Window*>(entry), 333 &temp_list, kRecentlyClosedCount); 334 } 335 } 336 // Lock recently_closed_pages and copy temp_list into it. 337 { 338 base::AutoLock auto_lock(list_lock_); 339 recently_closed_pages_ = temp_list; 340 } 341 342 // Send a query that retrieves the first favicon. 343 StartLoadingFavicon(); 344} 345 346void JumpList::TabRestoreServiceDestroyed(TabRestoreService* service) { 347} 348 349bool JumpList::AddTab(const TabRestoreService::Tab* tab, 350 ShellLinkItemList* list, 351 size_t max_items) { 352 // This code adds the URL and the title strings of the given tab to the 353 // specified list. 354 if (list->size() >= max_items) 355 return false; 356 357 scoped_refptr<ShellLinkItem> link = CreateShellLink(); 358 const sessions::SerializedNavigationEntry& current_navigation = 359 tab->navigations.at(tab->current_navigation_index); 360 std::string url = current_navigation.virtual_url().spec(); 361 link->GetCommandLine()->AppendArgNative(base::UTF8ToWide(url)); 362 link->set_title(current_navigation.title()); 363 list->push_back(link); 364 icon_urls_.push_back(make_pair(url, link)); 365 return true; 366} 367 368void JumpList::AddWindow(const TabRestoreService::Window* window, 369 ShellLinkItemList* list, 370 size_t max_items) { 371 // This code enumerates al the tabs in the given window object and add their 372 // URLs and titles to the list. 373 DCHECK(!window->tabs.empty()); 374 375 for (size_t i = 0; i < window->tabs.size(); ++i) { 376 if (!AddTab(&window->tabs[i], list, max_items)) 377 return; 378 } 379} 380 381void JumpList::StartLoadingFavicon() { 382 GURL url; 383 { 384 base::AutoLock auto_lock(list_lock_); 385 if (icon_urls_.empty()) { 386 // No more favicons are needed by the application JumpList. Schedule a 387 // RunUpdate call. 388 BrowserThread::PostTask( 389 BrowserThread::FILE, FROM_HERE, 390 base::Bind(&JumpList::RunUpdate, this)); 391 return; 392 } 393 // Ask FaviconService if it has a favicon of a URL. 394 // When FaviconService has one, it will call OnFaviconDataAvailable(). 395 url = GURL(icon_urls_.front().first); 396 } 397 FaviconService* favicon_service = 398 FaviconServiceFactory::GetForProfile(profile_, Profile::EXPLICIT_ACCESS); 399 task_id_ = favicon_service->GetFaviconImageForPageURL( 400 url, 401 base::Bind(&JumpList::OnFaviconDataAvailable, base::Unretained(this)), 402 &cancelable_task_tracker_); 403} 404 405void JumpList::OnFaviconDataAvailable( 406 const favicon_base::FaviconImageResult& image_result) { 407 // If there is currently a favicon request in progress, it is now outdated, 408 // as we have received another, so nullify the handle from the old request. 409 task_id_ = base::CancelableTaskTracker::kBadTaskId; 410 // lock the list to set icon data and pop the url 411 { 412 base::AutoLock auto_lock(list_lock_); 413 // Attach the received data to the ShellLinkItem object. 414 // This data will be decoded by the RunUpdate method. 415 if (!image_result.image.IsEmpty()) { 416 if (!icon_urls_.empty() && icon_urls_.front().second) 417 icon_urls_.front().second->set_icon_data(image_result.image.AsBitmap()); 418 } 419 420 if (!icon_urls_.empty()) 421 icon_urls_.pop_front(); 422 } 423 // Check whether we need to load more favicons. 424 StartLoadingFavicon(); 425} 426 427void JumpList::RunUpdate() { 428 ShellLinkItemList local_most_visited_pages; 429 ShellLinkItemList local_recently_closed_pages; 430 431 { 432 base::AutoLock auto_lock(list_lock_); 433 // Make sure we are not out of date: if icon_urls_ is not empty, then 434 // another notification has been received since we processed this one 435 if (!icon_urls_.empty()) 436 return; 437 438 // Make local copies of lists so we can release the lock. 439 local_most_visited_pages = most_visited_pages_; 440 local_recently_closed_pages = recently_closed_pages_; 441 } 442 443 // Delete the directory which contains old icon files, rename the current 444 // icon directory, and create a new directory which contains new JumpList 445 // icon files. 446 base::FilePath icon_dir_old(icon_dir_.value() + L"Old"); 447 if (base::PathExists(icon_dir_old)) 448 base::DeleteFile(icon_dir_old, true); 449 base::Move(icon_dir_, icon_dir_old); 450 base::CreateDirectory(icon_dir_); 451 452 // Create temporary icon files for shortcuts in the "Most Visited" category. 453 CreateIconFiles(local_most_visited_pages); 454 455 // Create temporary icon files for shortcuts in the "Recently Closed" 456 // category. 457 CreateIconFiles(local_recently_closed_pages); 458 459 // We finished collecting all resources needed for updating an appliation 460 // JumpList. So, create a new JumpList and replace the current JumpList 461 // with it. 462 UpdateJumpList(app_id_.c_str(), local_most_visited_pages, 463 local_recently_closed_pages); 464} 465 466void JumpList::CreateIconFiles(const ShellLinkItemList& item_list) { 467 for (ShellLinkItemList::const_iterator item = item_list.begin(); 468 item != item_list.end(); ++item) { 469 base::FilePath icon_path; 470 if (CreateIconFile((*item)->icon_data(), icon_dir_, &icon_path)) 471 (*item)->set_icon(icon_path.value(), 0); 472 } 473} 474