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