1// Copyright 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/ui/toolbar/recent_tabs_sub_menu_model.h"
6
7#include "base/bind.h"
8#include "base/metrics/histogram.h"
9#include "base/prefs/scoped_user_pref_update.h"
10#include "base/strings/string_number_conversions.h"
11#include "base/strings/utf_string_conversions.h"
12#include "chrome/app/chrome_command_ids.h"
13#include "chrome/browser/favicon/favicon_service_factory.h"
14#include "chrome/browser/profiles/profile.h"
15#include "chrome/browser/search/search.h"
16#include "chrome/browser/sessions/session_restore.h"
17#include "chrome/browser/sessions/tab_restore_service.h"
18#include "chrome/browser/sessions/tab_restore_service_delegate.h"
19#include "chrome/browser/sessions/tab_restore_service_factory.h"
20#include "chrome/browser/sync/glue/synced_session.h"
21#include "chrome/browser/sync/open_tabs_ui_delegate.h"
22#include "chrome/browser/sync/profile_sync_service.h"
23#include "chrome/browser/sync/profile_sync_service_factory.h"
24#include "chrome/browser/ui/browser.h"
25#include "chrome/browser/ui/browser_commands.h"
26#include "chrome/browser/ui/tabs/tab_strip_model.h"
27#include "chrome/browser/ui/toolbar/wrench_menu_model.h"
28#include "chrome/common/pref_names.h"
29#include "chrome/grit/generated_resources.h"
30#include "components/favicon_base/favicon_types.h"
31#include "grit/browser_resources.h"
32#include "grit/theme_resources.h"
33#include "ui/base/accelerators/accelerator.h"
34#include "ui/base/l10n/l10n_util.h"
35#include "ui/base/resource/resource_bundle.h"
36#include "ui/resources/grit/ui_resources.h"
37
38#if defined(USE_ASH)
39#include "ash/accelerators/accelerator_table.h"
40#endif  // defined(USE_ASH)
41
42namespace {
43
44// Initial comamnd ID's for navigatable (and hence executable) tab/window menu
45// items.  The menumodel and storage structures are not 1-1:
46// - menumodel has "Recently closed" header, "No tabs from other devices",
47//   device section headers, separators, local and other devices' tab items, and
48//   local window items.
49// - |local_tab_navigation_items_| and |other_devices_tab_navigation_items_|
50// only have navigatabale/executable tab items.
51// - |local_window_items_| only has executable open window items.
52// Using initial command IDs for local tab, local window and other devices' tab
53// items makes it easier and less error-prone to manipulate the menumodel and
54// storage structures.  These ids must be bigger than the maximum possible
55// number of items in the menumodel, so that index of the last menu item doesn't
56// clash with these values when menu items are retrieved via
57// GetIndexOfCommandId().
58// The range of all command ID's used in RecentTabsSubMenuModel, including the
59// "Recently closed" headers, must be between
60// |WrenchMenuModel::kMinRecentTabsCommandId| i.e. 1001 and 1200
61// (|WrenchMenuModel::kMaxRecentTabsCommandId|) inclusively.
62const int kFirstLocalTabCommandId = WrenchMenuModel::kMinRecentTabsCommandId;
63const int kFirstLocalWindowCommandId = 1031;
64const int kFirstOtherDevicesTabCommandId = 1051;
65const int kMinDeviceNameCommandId = 1100;
66const int kMaxDeviceNameCommandId = 1110;
67
68// The maximum number of local recently closed entries (tab or window) to be
69// shown in the menu.
70const int kMaxLocalEntries = 8;
71
72// Comparator function for use with std::sort that will sort sessions by
73// descending modified_time (i.e., most recent first).
74bool SortSessionsByRecency(const browser_sync::SyncedSession* s1,
75                           const browser_sync::SyncedSession* s2) {
76  return s1->modified_time > s2->modified_time;
77}
78
79// Comparator function for use with std::sort that will sort tabs by
80// descending timestamp (i.e., most recent first).
81bool SortTabsByRecency(const SessionTab* t1, const SessionTab* t2) {
82  return t1->timestamp > t2->timestamp;
83}
84
85// Returns true if the command id identifies a tab menu item.
86bool IsTabModelCommandId(int command_id) {
87  return ((command_id >= kFirstLocalTabCommandId &&
88           command_id < kFirstLocalWindowCommandId) ||
89          (command_id >= kFirstOtherDevicesTabCommandId &&
90           command_id < kMinDeviceNameCommandId));
91}
92
93// Returns true if the command id identifies a window menu item.
94bool IsWindowModelCommandId(int command_id) {
95  return command_id >= kFirstLocalWindowCommandId &&
96         command_id < kFirstOtherDevicesTabCommandId;
97}
98
99bool IsDeviceNameCommandId(int command_id) {
100  return command_id >= kMinDeviceNameCommandId &&
101      command_id <= kMaxDeviceNameCommandId;
102}
103
104// Convert |tab_vector_index| to command id of menu item, with
105// |first_command_id| as the base command id.
106int TabVectorIndexToCommandId(int tab_vector_index, int first_command_id) {
107  int command_id = tab_vector_index + first_command_id;
108  DCHECK(IsTabModelCommandId(command_id));
109  return command_id;
110}
111
112// Convert |window_vector_index| to command id of menu item.
113int WindowVectorIndexToCommandId(int window_vector_index) {
114  int command_id = window_vector_index + kFirstLocalWindowCommandId;
115  DCHECK(IsWindowModelCommandId(command_id));
116  return command_id;
117}
118
119// Convert |command_id| of menu item to index in |local_window_items_|.
120int CommandIdToWindowVectorIndex(int command_id) {
121  DCHECK(IsWindowModelCommandId(command_id));
122  return command_id - kFirstLocalWindowCommandId;
123}
124
125}  // namespace
126
127enum RecentTabAction {
128  LOCAL_SESSION_TAB = 0,
129  OTHER_DEVICE_TAB,
130  RESTORE_WINDOW,
131  SHOW_MORE,
132  LIMIT_RECENT_TAB_ACTION
133};
134
135// An element in |RecentTabsSubMenuModel::local_tab_navigation_items_| or
136// |RecentTabsSubMenuModel::other_devices_tab_navigation_items_| that stores
137// the navigation information of a local or other devices' tab required to
138// restore the tab.
139struct RecentTabsSubMenuModel::TabNavigationItem {
140  TabNavigationItem() : tab_id(-1) {}
141
142  TabNavigationItem(const std::string& session_tag,
143                    const SessionID::id_type& tab_id,
144                    const base::string16& title,
145                    const GURL& url)
146      : session_tag(session_tag),
147        tab_id(tab_id),
148        title(title),
149        url(url) {}
150
151  // For use by std::set for sorting.
152  bool operator<(const TabNavigationItem& other) const {
153    return url < other.url;
154  }
155
156  // Empty for local tabs, non-empty for other devices' tabs.
157  std::string session_tag;
158  SessionID::id_type tab_id;  // -1 for invalid, >= 0 otherwise.
159  base::string16 title;
160  GURL url;
161};
162
163const int RecentTabsSubMenuModel::kRecentlyClosedHeaderCommandId = 1120;
164const int RecentTabsSubMenuModel::kDisabledRecentlyClosedHeaderCommandId = 1121;
165
166RecentTabsSubMenuModel::RecentTabsSubMenuModel(
167    ui::AcceleratorProvider* accelerator_provider,
168    Browser* browser,
169    browser_sync::OpenTabsUIDelegate* open_tabs_delegate)
170    : ui::SimpleMenuModel(this),
171      browser_(browser),
172      open_tabs_delegate_(open_tabs_delegate),
173      last_local_model_index_(-1),
174      default_favicon_(ui::ResourceBundle::GetSharedInstance().
175                       GetNativeImageNamed(IDR_DEFAULT_FAVICON)),
176      weak_ptr_factory_(this) {
177  // Invoke asynchronous call to load tabs from local last session, which does
178  // nothing if the tabs have already been loaded or they shouldn't be loaded.
179  // TabRestoreServiceChanged() will be called after the tabs are loaded.
180  TabRestoreService* service =
181      TabRestoreServiceFactory::GetForProfile(browser_->profile());
182  if (service) {
183    service->LoadTabsFromLastSession();
184
185  // TODO(sail): enable this when mac implements the dynamic menu, together with
186  // MenuModelDelegate::MenuStructureChanged().
187#if !defined(OS_MACOSX)
188    service->AddObserver(this);
189#endif
190  }
191
192  Build();
193
194  // Retrieve accelerator key for IDC_RESTORE_TAB now, because on ASH, it's not
195  // defined in |accelerator_provider|, but in shell, so simply retrieve it now
196  // for all ASH and non-ASH for use in |GetAcceleratorForCommandId|.
197#if defined(USE_ASH)
198  for (size_t i = 0; i < ash::kAcceleratorDataLength; ++i) {
199    const ash::AcceleratorData& accel_data = ash::kAcceleratorData[i];
200    if (accel_data.action == ash::RESTORE_TAB) {
201      reopen_closed_tab_accelerator_ = ui::Accelerator(accel_data.keycode,
202                                                       accel_data.modifiers);
203      break;
204    }
205  }
206#else
207  if (accelerator_provider) {
208    accelerator_provider->GetAcceleratorForCommandId(
209        IDC_RESTORE_TAB, &reopen_closed_tab_accelerator_);
210  }
211#endif  // defined(USE_ASH)
212}
213
214RecentTabsSubMenuModel::~RecentTabsSubMenuModel() {
215  TabRestoreService* service =
216      TabRestoreServiceFactory::GetForProfile(browser_->profile());
217  if (service)
218    service->RemoveObserver(this);
219}
220
221bool RecentTabsSubMenuModel::IsCommandIdChecked(int command_id) const {
222  return false;
223}
224
225bool RecentTabsSubMenuModel::IsCommandIdEnabled(int command_id) const {
226  if (command_id == kRecentlyClosedHeaderCommandId ||
227      command_id == kDisabledRecentlyClosedHeaderCommandId ||
228      command_id == IDC_RECENT_TABS_NO_DEVICE_TABS ||
229      IsDeviceNameCommandId(command_id)) {
230    return false;
231  }
232  return true;
233}
234
235bool RecentTabsSubMenuModel::GetAcceleratorForCommandId(
236    int command_id, ui::Accelerator* accelerator) {
237  // If there are no recently closed items, we show the accelerator beside
238  // the header, otherwise, we show it beside the first item underneath it.
239  int index_in_menu = GetIndexOfCommandId(command_id);
240  int header_index = GetIndexOfCommandId(kRecentlyClosedHeaderCommandId);
241  if ((command_id == kDisabledRecentlyClosedHeaderCommandId ||
242       (header_index != -1 && index_in_menu == header_index + 1)) &&
243      reopen_closed_tab_accelerator_.key_code() != ui::VKEY_UNKNOWN) {
244    *accelerator = reopen_closed_tab_accelerator_;
245    return true;
246  }
247  return false;
248}
249
250void RecentTabsSubMenuModel::ExecuteCommand(int command_id, int event_flags) {
251  if (command_id == IDC_SHOW_HISTORY) {
252    UMA_HISTOGRAM_ENUMERATION("WrenchMenu.RecentTabsSubMenu", SHOW_MORE,
253                              LIMIT_RECENT_TAB_ACTION);
254    // We show all "other devices" on the history page.
255    chrome::ExecuteCommandWithDisposition(browser_, IDC_SHOW_HISTORY,
256        ui::DispositionFromEventFlags(event_flags));
257    return;
258  }
259
260  DCHECK_NE(IDC_RECENT_TABS_NO_DEVICE_TABS, command_id);
261  DCHECK(!IsDeviceNameCommandId(command_id));
262
263  WindowOpenDisposition disposition =
264      ui::DispositionFromEventFlags(event_flags);
265  if (disposition == CURRENT_TAB)  // Force to open a new foreground tab.
266    disposition = NEW_FOREGROUND_TAB;
267
268  TabRestoreService* service =
269      TabRestoreServiceFactory::GetForProfile(browser_->profile());
270  TabRestoreServiceDelegate* delegate =
271      TabRestoreServiceDelegate::FindDelegateForWebContents(
272          browser_->tab_strip_model()->GetActiveWebContents());
273  if (IsTabModelCommandId(command_id)) {
274    TabNavigationItems* tab_items = NULL;
275    int tab_items_idx = CommandIdToTabVectorIndex(command_id, &tab_items);
276    const TabNavigationItem& item = (*tab_items)[tab_items_idx];
277    DCHECK(item.tab_id > -1 && item.url.is_valid());
278
279    if (item.session_tag.empty()) {  // Restore tab of local session.
280      if (service && delegate) {
281        UMA_HISTOGRAM_ENUMERATION("WrenchMenu.RecentTabsSubMenu",
282                                  LOCAL_SESSION_TAB, LIMIT_RECENT_TAB_ACTION);
283        service->RestoreEntryById(delegate, item.tab_id,
284                                  browser_->host_desktop_type(), disposition);
285      }
286    } else {  // Restore tab of session from other devices.
287      browser_sync::OpenTabsUIDelegate* open_tabs = GetOpenTabsUIDelegate();
288      if (!open_tabs)
289        return;
290      const SessionTab* tab;
291      if (!open_tabs->GetForeignTab(item.session_tag, item.tab_id, &tab))
292        return;
293      if (tab->navigations.empty())
294        return;
295      UMA_HISTOGRAM_ENUMERATION("WrenchMenu.RecentTabsSubMenu",
296                                OTHER_DEVICE_TAB, LIMIT_RECENT_TAB_ACTION);
297      SessionRestore::RestoreForeignSessionTab(
298          browser_->tab_strip_model()->GetActiveWebContents(),
299          *tab, disposition);
300    }
301  } else {
302    DCHECK(IsWindowModelCommandId(command_id));
303    if (service && delegate) {
304      int window_items_idx = CommandIdToWindowVectorIndex(command_id);
305      DCHECK(window_items_idx >= 0 &&
306             window_items_idx < static_cast<int>(local_window_items_.size()));
307      UMA_HISTOGRAM_ENUMERATION("WrenchMenu.RecentTabsSubMenu", RESTORE_WINDOW,
308                                LIMIT_RECENT_TAB_ACTION);
309      service->RestoreEntryById(delegate, local_window_items_[window_items_idx],
310                                browser_->host_desktop_type(), disposition);
311    }
312  }
313}
314
315int RecentTabsSubMenuModel::GetFirstRecentTabsCommandId() {
316  return WindowVectorIndexToCommandId(0);
317}
318
319const gfx::FontList* RecentTabsSubMenuModel::GetLabelFontListAt(
320    int index) const {
321  int command_id = GetCommandIdAt(index);
322  if (command_id == kRecentlyClosedHeaderCommandId ||
323      IsDeviceNameCommandId(command_id)) {
324    return &ui::ResourceBundle::GetSharedInstance().GetFontList(
325        ui::ResourceBundle::BoldFont);
326  }
327  return NULL;
328}
329
330int RecentTabsSubMenuModel::GetMaxWidthForItemAtIndex(int item_index) const {
331  int command_id = GetCommandIdAt(item_index);
332  if (command_id == IDC_RECENT_TABS_NO_DEVICE_TABS ||
333      command_id == kRecentlyClosedHeaderCommandId ||
334      command_id == kDisabledRecentlyClosedHeaderCommandId) {
335    return -1;
336  }
337  return 320;
338}
339
340bool RecentTabsSubMenuModel::GetURLAndTitleForItemAtIndex(
341    int index,
342    std::string* url,
343    base::string16* title) {
344  int command_id = GetCommandIdAt(index);
345  if (IsTabModelCommandId(command_id)) {
346    TabNavigationItems* tab_items = NULL;
347    int tab_items_idx = CommandIdToTabVectorIndex(command_id, &tab_items);
348    const TabNavigationItem& item = (*tab_items)[tab_items_idx];
349    *url = item.url.possibly_invalid_spec();
350    *title = item.title;
351    return true;
352  }
353  return false;
354}
355
356void RecentTabsSubMenuModel::Build() {
357  // The menu contains:
358  // - Recently closed header, then list of local recently closed tabs/windows,
359  //   then separator
360  // - device 1 section header, then list of tabs from device, then separator
361  // - device 2 section header, then list of tabs from device, then separator
362  // - device 3 section header, then list of tabs from device, then separator
363  // - More... to open the history tab to get more other devices.
364  // |local_tab_navigation_items_| and |other_devices_tab_navigation_items_|
365  // only contain navigatable (and hence executable) tab items for local
366  // recently closed tabs and tabs from other devices respectively.
367  // |local_window_items_| contains the local recently closed windows.
368  BuildLocalEntries();
369  BuildTabsFromOtherDevices();
370}
371
372void RecentTabsSubMenuModel::BuildLocalEntries() {
373  // All local items use InsertItem*At() to append or insert a menu item.
374  // We're appending if building the entries for the first time i.e. invoked
375  // from Constructor(), inserting when local entries change subsequently i.e.
376  // invoked from TabRestoreServiceChanged().
377
378  DCHECK_EQ(last_local_model_index_, -1);
379
380  TabRestoreService* service =
381      TabRestoreServiceFactory::GetForProfile(browser_->profile());
382  if (!service || service->entries().size() == 0) {
383    // This is to show a disabled restore tab entry with the accelerator to
384    // teach users about this command.
385    InsertItemWithStringIdAt(++last_local_model_index_,
386                             kDisabledRecentlyClosedHeaderCommandId,
387                             IDS_NEW_TAB_RECENTLY_CLOSED);
388  } else {
389    InsertItemWithStringIdAt(++last_local_model_index_,
390                             kRecentlyClosedHeaderCommandId,
391                             IDS_NEW_TAB_RECENTLY_CLOSED);
392    ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
393    SetIcon(last_local_model_index_,
394            rb.GetNativeImageNamed(IDR_RECENTLY_CLOSED_WINDOW));
395
396    int added_count = 0;
397    TabRestoreService::Entries entries = service->entries();
398    for (TabRestoreService::Entries::const_iterator it = entries.begin();
399         it != entries.end() && added_count < kMaxLocalEntries; ++it) {
400      TabRestoreService::Entry* entry = *it;
401      if (entry->type == TabRestoreService::TAB) {
402        TabRestoreService::Tab* tab =
403            static_cast<TabRestoreService::Tab*>(entry);
404        const sessions::SerializedNavigationEntry& current_navigation =
405            tab->navigations.at(tab->current_navigation_index);
406        BuildLocalTabItem(
407            entry->id,
408            current_navigation.title(),
409            current_navigation.virtual_url(),
410            ++last_local_model_index_);
411      } else  {
412        DCHECK_EQ(entry->type, TabRestoreService::WINDOW);
413        BuildLocalWindowItem(
414            entry->id,
415            static_cast<TabRestoreService::Window*>(entry)->tabs.size(),
416            ++last_local_model_index_);
417      }
418      ++added_count;
419    }
420  }
421
422  DCHECK_GE(last_local_model_index_, 0);
423}
424
425void RecentTabsSubMenuModel::BuildTabsFromOtherDevices() {
426  // All other devices' items (device headers or tabs) use AddItem*() to append
427  // a menu item, because they are always only built once (i.e. invoked from
428  // Constructor()) and don't change after that.
429
430  browser_sync::OpenTabsUIDelegate* open_tabs = GetOpenTabsUIDelegate();
431  std::vector<const browser_sync::SyncedSession*> sessions;
432  if (!open_tabs || !open_tabs->GetAllForeignSessions(&sessions)) {
433    AddSeparator(ui::NORMAL_SEPARATOR);
434    AddItemWithStringId(IDC_RECENT_TABS_NO_DEVICE_TABS,
435                        IDS_RECENT_TABS_NO_DEVICE_TABS);
436    return;
437  }
438
439  // Sort sessions from most recent to least recent.
440  std::sort(sessions.begin(), sessions.end(), SortSessionsByRecency);
441
442  const size_t kMaxSessionsToShow = 3;
443  size_t num_sessions_added = 0;
444  for (size_t i = 0;
445       i < sessions.size() && num_sessions_added < kMaxSessionsToShow; ++i) {
446    const browser_sync::SyncedSession* session = sessions[i];
447    const std::string& session_tag = session->session_tag;
448
449    // Get windows of session.
450    std::vector<const SessionWindow*> windows;
451    if (!open_tabs->GetForeignSession(session_tag, &windows) ||
452        windows.empty()) {
453      continue;
454    }
455
456    // Collect tabs from all windows of session, pruning those that are not
457    // syncable or are NewTabPage, then sort them from most recent to least
458    // recent, independent of which window the tabs were from.
459    std::vector<const SessionTab*> tabs_in_session;
460    for (size_t j = 0; j < windows.size(); ++j) {
461      const SessionWindow* window = windows[j];
462      for (size_t t = 0; t < window->tabs.size(); ++t) {
463        const SessionTab* tab = window->tabs[t];
464        if (tab->navigations.empty())
465          continue;
466        const sessions::SerializedNavigationEntry& current_navigation =
467            tab->navigations.at(tab->normalized_navigation_index());
468        if (chrome::IsNTPURL(current_navigation.virtual_url(),
469                             browser_->profile())) {
470          continue;
471        }
472        tabs_in_session.push_back(tab);
473      }
474    }
475    if (tabs_in_session.empty())
476      continue;
477    std::sort(tabs_in_session.begin(), tabs_in_session.end(),
478              SortTabsByRecency);
479
480    // Add the header for the device session.
481    DCHECK(!session->session_name.empty());
482    AddSeparator(ui::NORMAL_SEPARATOR);
483    int command_id = kMinDeviceNameCommandId + i;
484    DCHECK_LE(command_id, kMaxDeviceNameCommandId);
485    AddItem(command_id, base::UTF8ToUTF16(session->session_name));
486    AddDeviceFavicon(GetItemCount() - 1, session->device_type);
487
488    // Build tab menu items from sorted session tabs.
489    const size_t kMaxTabsPerSessionToShow = 4;
490    for (size_t k = 0;
491         k < std::min(tabs_in_session.size(), kMaxTabsPerSessionToShow);
492         ++k) {
493      BuildOtherDevicesTabItem(session_tag, *tabs_in_session[k]);
494    }  // for all tabs in one session
495
496    ++num_sessions_added;
497  }  // for all sessions
498
499  // We are not supposed to get here unless at least some items were added.
500  DCHECK_GT(GetItemCount(), 0);
501  AddSeparator(ui::NORMAL_SEPARATOR);
502  AddItemWithStringId(IDC_SHOW_HISTORY, IDS_RECENT_TABS_MORE);
503}
504
505void RecentTabsSubMenuModel::BuildLocalTabItem(int session_id,
506                                               const base::string16& title,
507                                               const GURL& url,
508                                               int curr_model_index) {
509  TabNavigationItem item(std::string(), session_id, title, url);
510  int command_id = TabVectorIndexToCommandId(
511      local_tab_navigation_items_.size(), kFirstLocalTabCommandId);
512  // See comments in BuildLocalEntries() about usage of InsertItem*At().
513  // There may be no tab title, in which case, use the url as tab title.
514  InsertItemAt(curr_model_index, command_id,
515               title.empty() ? base::UTF8ToUTF16(item.url.spec()) : title);
516  AddTabFavicon(command_id, item.url);
517  local_tab_navigation_items_.push_back(item);
518}
519
520void RecentTabsSubMenuModel::BuildLocalWindowItem(
521    const SessionID::id_type& window_id,
522    int num_tabs,
523    int curr_model_index) {
524  int command_id = WindowVectorIndexToCommandId(local_window_items_.size());
525  // See comments in BuildLocalEntries() about usage of InsertItem*At().
526  if (num_tabs == 1) {
527    InsertItemWithStringIdAt(curr_model_index, command_id,
528                             IDS_NEW_TAB_RECENTLY_CLOSED_WINDOW_SINGLE);
529  } else {
530    InsertItemAt(curr_model_index, command_id, l10n_util::GetStringFUTF16(
531        IDS_NEW_TAB_RECENTLY_CLOSED_WINDOW_MULTIPLE,
532        base::IntToString16(num_tabs)));
533  }
534  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
535  SetIcon(curr_model_index, rb.GetNativeImageNamed(IDR_RECENTLY_CLOSED_WINDOW));
536  local_window_items_.push_back(window_id);
537}
538
539void RecentTabsSubMenuModel::BuildOtherDevicesTabItem(
540    const std::string& session_tag,
541    const SessionTab& tab) {
542  const sessions::SerializedNavigationEntry& current_navigation =
543      tab.navigations.at(tab.normalized_navigation_index());
544  TabNavigationItem item(session_tag, tab.tab_id.id(),
545                         current_navigation.title(),
546                         current_navigation.virtual_url());
547  int command_id = TabVectorIndexToCommandId(
548      other_devices_tab_navigation_items_.size(),
549      kFirstOtherDevicesTabCommandId);
550  // See comments in BuildTabsFromOtherDevices() about usage of AddItem*().
551  // There may be no tab title, in which case, use the url as tab title.
552  AddItem(command_id,
553          current_navigation.title().empty() ?
554              base::UTF8ToUTF16(item.url.spec()) : current_navigation.title());
555  AddTabFavicon(command_id, item.url);
556  other_devices_tab_navigation_items_.push_back(item);
557}
558
559void RecentTabsSubMenuModel::AddDeviceFavicon(
560    int index_in_menu,
561    browser_sync::SyncedSession::DeviceType device_type) {
562  int favicon_id = -1;
563  switch (device_type) {
564    case browser_sync::SyncedSession::TYPE_PHONE:
565      favicon_id = IDR_PHONE_FAVICON;
566      break;
567
568    case browser_sync::SyncedSession::TYPE_TABLET:
569      favicon_id = IDR_TABLET_FAVICON;
570      break;
571
572    case browser_sync::SyncedSession::TYPE_CHROMEOS:
573    case browser_sync::SyncedSession::TYPE_WIN:
574    case browser_sync::SyncedSession::TYPE_MACOSX:
575    case browser_sync::SyncedSession::TYPE_LINUX:
576    case browser_sync::SyncedSession::TYPE_OTHER:
577    case browser_sync::SyncedSession::TYPE_UNSET:
578      favicon_id = IDR_LAPTOP_FAVICON;
579      break;
580  }
581
582  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
583  SetIcon(index_in_menu, rb.GetNativeImageNamed(favicon_id));
584}
585
586void RecentTabsSubMenuModel::AddTabFavicon(int command_id, const GURL& url) {
587  bool is_local_tab = command_id < kFirstOtherDevicesTabCommandId;
588  int index_in_menu = GetIndexOfCommandId(command_id);
589
590  if (!is_local_tab) {
591    // If tab has synced favicon, use it.
592    // Note that currently, other devices' tabs only have favicons if
593    // --sync-tab-favicons switch is on; according to zea@, this flag is now
594    // automatically enabled for iOS and android, and they're looking into
595    // enabling it for other platforms.
596    browser_sync::OpenTabsUIDelegate* open_tabs = GetOpenTabsUIDelegate();
597    scoped_refptr<base::RefCountedMemory> favicon_png;
598    if (open_tabs &&
599        open_tabs->GetSyncedFaviconForPageURL(url.spec(), &favicon_png)) {
600      gfx::Image image = gfx::Image::CreateFrom1xPNGBytes(favicon_png);
601      SetIcon(index_in_menu, image);
602      return;
603    }
604  }
605
606  // Otherwise, start to fetch the favicon from local history asynchronously.
607  // Set default icon first.
608  SetIcon(index_in_menu, default_favicon_);
609  // Start request to fetch actual icon if possible.
610  FaviconService* favicon_service = FaviconServiceFactory::GetForProfile(
611      browser_->profile(), Profile::EXPLICIT_ACCESS);
612  if (!favicon_service)
613    return;
614
615  favicon_service->GetFaviconImageForPageURL(
616      url,
617      base::Bind(&RecentTabsSubMenuModel::OnFaviconDataAvailable,
618                 weak_ptr_factory_.GetWeakPtr(),
619                 command_id),
620      is_local_tab ? &local_tab_cancelable_task_tracker_
621                   : &other_devices_tab_cancelable_task_tracker_);
622}
623
624void RecentTabsSubMenuModel::OnFaviconDataAvailable(
625    int command_id,
626    const favicon_base::FaviconImageResult& image_result) {
627  if (image_result.image.IsEmpty())
628    return;
629  int index_in_menu = GetIndexOfCommandId(command_id);
630  DCHECK_GT(index_in_menu, -1);
631  SetIcon(index_in_menu, image_result.image);
632  ui::MenuModelDelegate* menu_model_delegate = GetMenuModelDelegate();
633  if (menu_model_delegate)
634    menu_model_delegate->OnIconChanged(index_in_menu);
635}
636
637int RecentTabsSubMenuModel::CommandIdToTabVectorIndex(
638    int command_id,
639    TabNavigationItems** tab_items) {
640  DCHECK(IsTabModelCommandId(command_id));
641  if (command_id >= kFirstOtherDevicesTabCommandId) {
642    *tab_items = &other_devices_tab_navigation_items_;
643    return command_id - kFirstOtherDevicesTabCommandId;
644  }
645  *tab_items = &local_tab_navigation_items_;
646  return command_id - kFirstLocalTabCommandId;
647}
648
649void RecentTabsSubMenuModel::ClearLocalEntries() {
650  // Remove local items (recently closed tabs and windows) from menumodel.
651  while (last_local_model_index_ >= 0)
652    RemoveItemAt(last_local_model_index_--);
653
654  // Cancel asynchronous FaviconService::GetFaviconImageForPageURL() tasks of
655  // all
656  // local tabs.
657  local_tab_cancelable_task_tracker_.TryCancelAll();
658
659  // Remove all local tab navigation items.
660  local_tab_navigation_items_.clear();
661
662  // Remove all local window items.
663  local_window_items_.clear();
664}
665
666browser_sync::OpenTabsUIDelegate*
667    RecentTabsSubMenuModel::GetOpenTabsUIDelegate() {
668  if (!open_tabs_delegate_) {
669    ProfileSyncService* service = ProfileSyncServiceFactory::GetInstance()->
670        GetForProfile(browser_->profile());
671    // Only return the delegate if it exists and it is done syncing sessions.
672    if (service && service->ShouldPushChanges())
673      open_tabs_delegate_ = service->GetOpenTabsUIDelegate();
674  }
675  return open_tabs_delegate_;
676}
677
678void RecentTabsSubMenuModel::TabRestoreServiceChanged(
679    TabRestoreService* service) {
680  ClearLocalEntries();
681
682  BuildLocalEntries();
683
684  ui::MenuModelDelegate* menu_model_delegate = GetMenuModelDelegate();
685  if (menu_model_delegate)
686    menu_model_delegate->OnMenuStructureChanged();
687}
688
689void RecentTabsSubMenuModel::TabRestoreServiceDestroyed(
690    TabRestoreService* service) {
691  TabRestoreServiceChanged(service);
692}
693