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/ui/webui/ntp/foreign_session_handler.h"
6
7#include <algorithm>
8#include <string>
9#include <vector>
10
11#include "base/bind.h"
12#include "base/bind_helpers.h"
13#include "base/i18n/time_formatting.h"
14#include "base/memory/scoped_vector.h"
15#include "base/prefs/pref_service.h"
16#include "base/prefs/scoped_user_pref_update.h"
17#include "base/strings/string_number_conversions.h"
18#include "base/strings/utf_string_conversions.h"
19#include "base/values.h"
20#include "chrome/browser/chrome_notification_types.h"
21#include "chrome/browser/profiles/profile.h"
22#include "chrome/browser/sessions/session_restore.h"
23#include "chrome/browser/sync/profile_sync_service.h"
24#include "chrome/browser/sync/profile_sync_service_factory.h"
25#include "chrome/browser/ui/host_desktop.h"
26#include "chrome/browser/ui/webui/ntp/new_tab_ui.h"
27#include "chrome/common/pref_names.h"
28#include "chrome/common/url_constants.h"
29#include "chrome/grit/generated_resources.h"
30#include "components/pref_registry/pref_registry_syncable.h"
31#include "content/public/browser/notification_service.h"
32#include "content/public/browser/notification_source.h"
33#include "content/public/browser/url_data_source.h"
34#include "content/public/browser/web_contents.h"
35#include "content/public/browser/web_ui.h"
36#include "ui/base/l10n/l10n_util.h"
37#include "ui/base/l10n/time_format.h"
38#include "ui/base/webui/web_ui_util.h"
39
40namespace browser_sync {
41
42// Maximum number of sessions we're going to display on the NTP
43static const size_t kMaxSessionsToShow = 10;
44
45namespace {
46
47// Comparator function for use with std::sort that will sort sessions by
48// descending modified_time (i.e., most recent first).
49bool SortSessionsByRecency(const SyncedSession* s1, const SyncedSession* s2) {
50  return s1->modified_time > s2->modified_time;
51}
52
53}  // namepace
54
55ForeignSessionHandler::ForeignSessionHandler() {
56}
57
58// static
59void ForeignSessionHandler::RegisterProfilePrefs(
60    user_prefs::PrefRegistrySyncable* registry) {
61  registry->RegisterDictionaryPref(
62      prefs::kNtpCollapsedForeignSessions,
63      user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF);
64}
65
66// static
67void ForeignSessionHandler::OpenForeignSessionTab(
68    content::WebUI* web_ui,
69    const std::string& session_string_value,
70    SessionID::id_type window_num,
71    SessionID::id_type tab_id,
72    const WindowOpenDisposition& disposition) {
73  OpenTabsUIDelegate* open_tabs = GetOpenTabsUIDelegate(web_ui);
74  if (!open_tabs)
75    return;
76
77  // We don't actually care about |window_num|, this is just a sanity check.
78  DCHECK_LT(kInvalidId, window_num);
79  const SessionTab* tab;
80  if (!open_tabs->GetForeignTab(session_string_value, tab_id, &tab)) {
81    LOG(ERROR) << "Failed to load foreign tab.";
82    return;
83  }
84  if (tab->navigations.empty()) {
85    LOG(ERROR) << "Foreign tab no longer has valid navigations.";
86    return;
87  }
88  SessionRestore::RestoreForeignSessionTab(
89      web_ui->GetWebContents(), *tab, disposition);
90}
91
92// static
93void ForeignSessionHandler::OpenForeignSessionWindows(
94    content::WebUI* web_ui,
95    const std::string& session_string_value,
96    SessionID::id_type window_num) {
97  OpenTabsUIDelegate* open_tabs = GetOpenTabsUIDelegate(web_ui);
98  if (!open_tabs)
99    return;
100
101  std::vector<const SessionWindow*> windows;
102  // Note: we don't own the ForeignSessions themselves.
103  if (!open_tabs->GetForeignSession(session_string_value, &windows)) {
104    LOG(ERROR) << "ForeignSessionHandler failed to get session data from"
105        "OpenTabsUIDelegate.";
106    return;
107  }
108  std::vector<const SessionWindow*>::const_iterator iter_begin =
109      windows.begin() + (window_num == kInvalidId ? 0 : window_num);
110  std::vector<const SessionWindow*>::const_iterator iter_end =
111      window_num == kInvalidId ?
112      std::vector<const SessionWindow*>::const_iterator(windows.end()) :
113      iter_begin + 1;
114  chrome::HostDesktopType host_desktop_type =
115      chrome::GetHostDesktopTypeForNativeView(
116          web_ui->GetWebContents()->GetNativeView());
117  SessionRestore::RestoreForeignSessionWindows(
118      Profile::FromWebUI(web_ui), host_desktop_type, iter_begin, iter_end);
119}
120
121// static
122bool ForeignSessionHandler::SessionTabToValue(
123    const SessionTab& tab,
124    base::DictionaryValue* dictionary) {
125  if (tab.navigations.empty())
126    return false;
127
128  int selected_index = std::min(tab.current_navigation_index,
129                                static_cast<int>(tab.navigations.size() - 1));
130  const ::sessions::SerializedNavigationEntry& current_navigation =
131      tab.navigations.at(selected_index);
132  GURL tab_url = current_navigation.virtual_url();
133  if (tab_url == GURL(chrome::kChromeUINewTabURL))
134    return false;
135
136  NewTabUI::SetUrlTitleAndDirection(dictionary, current_navigation.title(),
137                                    tab_url);
138  dictionary->SetString("type", "tab");
139  dictionary->SetDouble("timestamp",
140                        static_cast<double>(tab.timestamp.ToInternalValue()));
141  // TODO(jeremycho): This should probably be renamed to tabId to avoid
142  // confusion with the ID corresponding to a session.  Investigate all the
143  // places (C++ and JS) where this is being used.  (http://crbug.com/154865).
144  dictionary->SetInteger("sessionId", tab.tab_id.id());
145  return true;
146}
147
148// static
149OpenTabsUIDelegate* ForeignSessionHandler::GetOpenTabsUIDelegate(
150    content::WebUI* web_ui) {
151  Profile* profile = Profile::FromWebUI(web_ui);
152  ProfileSyncService* service =
153      ProfileSyncServiceFactory::GetInstance()->GetForProfile(profile);
154
155  // Only return the delegate if it exists and it is done syncing sessions.
156  if (service && service->ShouldPushChanges())
157    return service->GetOpenTabsUIDelegate();
158
159  return NULL;
160}
161
162void ForeignSessionHandler::RegisterMessages() {
163  Init();
164  web_ui()->RegisterMessageCallback("deleteForeignSession",
165      base::Bind(&ForeignSessionHandler::HandleDeleteForeignSession,
166                 base::Unretained(this)));
167  web_ui()->RegisterMessageCallback("getForeignSessions",
168      base::Bind(&ForeignSessionHandler::HandleGetForeignSessions,
169                 base::Unretained(this)));
170  web_ui()->RegisterMessageCallback("openForeignSession",
171      base::Bind(&ForeignSessionHandler::HandleOpenForeignSession,
172                 base::Unretained(this)));
173  web_ui()->RegisterMessageCallback("setForeignSessionCollapsed",
174      base::Bind(&ForeignSessionHandler::HandleSetForeignSessionCollapsed,
175                 base::Unretained(this)));
176}
177
178void ForeignSessionHandler::Init() {
179  Profile* profile = Profile::FromWebUI(web_ui());
180  ProfileSyncService* service =
181      ProfileSyncServiceFactory::GetInstance()->GetForProfile(profile);
182  registrar_.Add(this, chrome::NOTIFICATION_SYNC_CONFIGURE_DONE,
183                 content::Source<ProfileSyncService>(service));
184  registrar_.Add(this, chrome::NOTIFICATION_FOREIGN_SESSION_UPDATED,
185                 content::Source<Profile>(profile));
186  registrar_.Add(this, chrome::NOTIFICATION_FOREIGN_SESSION_DISABLED,
187                 content::Source<Profile>(profile));
188}
189
190void ForeignSessionHandler::Observe(
191    int type,
192    const content::NotificationSource& source,
193    const content::NotificationDetails& details) {
194  base::ListValue list_value;
195
196  switch (type) {
197    case chrome::NOTIFICATION_FOREIGN_SESSION_DISABLED:
198      // Tab sync is disabled, so clean up data about collapsed sessions.
199      Profile::FromWebUI(web_ui())->GetPrefs()->ClearPref(
200          prefs::kNtpCollapsedForeignSessions);
201      // Fall through.
202    case chrome::NOTIFICATION_SYNC_CONFIGURE_DONE:
203    case chrome::NOTIFICATION_FOREIGN_SESSION_UPDATED:
204      HandleGetForeignSessions(&list_value);
205      break;
206    default:
207      NOTREACHED();
208  }
209}
210
211
212bool ForeignSessionHandler::IsTabSyncEnabled() {
213  Profile* profile = Profile::FromWebUI(web_ui());
214  ProfileSyncService* service =
215      ProfileSyncServiceFactory::GetInstance()->GetForProfile(profile);
216  return service && service->GetActiveDataTypes().Has(syncer::PROXY_TABS);
217}
218
219base::string16 ForeignSessionHandler::FormatSessionTime(
220    const base::Time& time) {
221  // Return a time like "1 hour ago", "2 days ago", etc.
222  base::Time now = base::Time::Now();
223  // TimeFormat does not support negative TimeDelta values, so then we use 0.
224  return ui::TimeFormat::Simple(
225      ui::TimeFormat::FORMAT_ELAPSED, ui::TimeFormat::LENGTH_SHORT,
226      now < time ? base::TimeDelta() : now - time);
227}
228
229void ForeignSessionHandler::HandleGetForeignSessions(
230    const base::ListValue* args) {
231  OpenTabsUIDelegate* open_tabs = GetOpenTabsUIDelegate(web_ui());
232  std::vector<const SyncedSession*> sessions;
233
234  base::ListValue session_list;
235  if (open_tabs && open_tabs->GetAllForeignSessions(&sessions)) {
236    // Sort sessions from most recent to least recent.
237    std::sort(sessions.begin(), sessions.end(), SortSessionsByRecency);
238
239    // Use a pref to keep track of sessions that were collapsed by the user.
240    // To prevent the pref from accumulating stale sessions, clear it each time
241    // and only add back sessions that are still current.
242    DictionaryPrefUpdate pref_update(Profile::FromWebUI(web_ui())->GetPrefs(),
243                                     prefs::kNtpCollapsedForeignSessions);
244    base::DictionaryValue* current_collapsed_sessions = pref_update.Get();
245    scoped_ptr<base::DictionaryValue> collapsed_sessions(
246        current_collapsed_sessions->DeepCopy());
247    current_collapsed_sessions->Clear();
248
249    // Note: we don't own the SyncedSessions themselves.
250    for (size_t i = 0; i < sessions.size() && i < kMaxSessionsToShow; ++i) {
251      const SyncedSession* session = sessions[i];
252      const std::string& session_tag = session->session_tag;
253      scoped_ptr<base::DictionaryValue> session_data(
254          new base::DictionaryValue());
255      session_data->SetString("tag", session_tag);
256      session_data->SetString("name", session->session_name);
257      session_data->SetString("deviceType", session->DeviceTypeAsString());
258      session_data->SetString("modifiedTime",
259                              FormatSessionTime(session->modified_time));
260
261      bool is_collapsed = collapsed_sessions->HasKey(session_tag);
262      session_data->SetBoolean("collapsed", is_collapsed);
263      if (is_collapsed)
264        current_collapsed_sessions->SetBoolean(session_tag, true);
265
266      scoped_ptr<base::ListValue> window_list(new base::ListValue());
267      for (SyncedSession::SyncedWindowMap::const_iterator it =
268           session->windows.begin(); it != session->windows.end(); ++it) {
269        SessionWindow* window = it->second;
270        scoped_ptr<base::DictionaryValue> window_data(
271            new base::DictionaryValue());
272        if (SessionWindowToValue(*window, window_data.get()))
273          window_list->Append(window_data.release());
274      }
275
276      session_data->Set("windows", window_list.release());
277      session_list.Append(session_data.release());
278    }
279  }
280  base::FundamentalValue tab_sync_enabled(IsTabSyncEnabled());
281  web_ui()->CallJavascriptFunction("ntp.setForeignSessions",
282                                   session_list,
283                                   tab_sync_enabled);
284}
285
286void ForeignSessionHandler::HandleOpenForeignSession(
287    const base::ListValue* args) {
288  size_t num_args = args->GetSize();
289  // Expect either 1 or 8 args. For restoring an entire session, only
290  // one argument is required -- the session tag. To restore a tab,
291  // the additional args required are the window id, the tab id,
292  // and 4 properties of the event object (button, altKey, ctrlKey,
293  // metaKey, shiftKey) for determining how to open the tab.
294  if (num_args != 8U && num_args != 1U) {
295    LOG(ERROR) << "openForeignSession called with " << args->GetSize()
296               << " arguments.";
297    return;
298  }
299
300  // Extract the session tag (always provided).
301  std::string session_string_value;
302  if (!args->GetString(0, &session_string_value)) {
303    LOG(ERROR) << "Failed to extract session tag.";
304    return;
305  }
306
307  // Extract window number.
308  std::string window_num_str;
309  int window_num = kInvalidId;
310  if (num_args >= 2 && (!args->GetString(1, &window_num_str) ||
311      !base::StringToInt(window_num_str, &window_num))) {
312    LOG(ERROR) << "Failed to extract window number.";
313    return;
314  }
315
316  // Extract tab id.
317  std::string tab_id_str;
318  SessionID::id_type tab_id = kInvalidId;
319  if (num_args >= 3 && (!args->GetString(2, &tab_id_str) ||
320      !base::StringToInt(tab_id_str, &tab_id))) {
321    LOG(ERROR) << "Failed to extract tab SessionID.";
322    return;
323  }
324
325  if (tab_id != kInvalidId) {
326    WindowOpenDisposition disposition = webui::GetDispositionFromClick(args, 3);
327    OpenForeignSessionTab(
328        web_ui(), session_string_value, window_num, tab_id, disposition);
329  } else {
330    OpenForeignSessionWindows(web_ui(), session_string_value, window_num);
331  }
332}
333
334void ForeignSessionHandler::HandleDeleteForeignSession(
335    const base::ListValue* args) {
336  if (args->GetSize() != 1U) {
337    LOG(ERROR) << "Wrong number of args to deleteForeignSession";
338    return;
339  }
340
341  // Get the session tag argument (required).
342  std::string session_tag;
343  if (!args->GetString(0, &session_tag)) {
344    LOG(ERROR) << "Unable to extract session tag";
345    return;
346  }
347
348  OpenTabsUIDelegate* open_tabs = GetOpenTabsUIDelegate(web_ui());
349  if (open_tabs)
350    open_tabs->DeleteForeignSession(session_tag);
351}
352
353void ForeignSessionHandler::HandleSetForeignSessionCollapsed(
354    const base::ListValue* args) {
355  if (args->GetSize() != 2U) {
356    LOG(ERROR) << "Wrong number of args to setForeignSessionCollapsed";
357    return;
358  }
359
360  // Get the session tag argument (required).
361  std::string session_tag;
362  if (!args->GetString(0, &session_tag)) {
363    LOG(ERROR) << "Unable to extract session tag";
364    return;
365  }
366
367  bool is_collapsed;
368  if (!args->GetBoolean(1, &is_collapsed)) {
369    LOG(ERROR) << "Unable to extract boolean argument";
370    return;
371  }
372
373  // Store session tags for collapsed sessions in a preference so that the
374  // collapsed state persists.
375  PrefService* prefs = Profile::FromWebUI(web_ui())->GetPrefs();
376  DictionaryPrefUpdate update(prefs, prefs::kNtpCollapsedForeignSessions);
377  if (is_collapsed)
378    update.Get()->SetBoolean(session_tag, true);
379  else
380    update.Get()->Remove(session_tag, NULL);
381}
382
383bool ForeignSessionHandler::SessionWindowToValue(
384    const SessionWindow& window,
385    base::DictionaryValue* dictionary) {
386  if (window.tabs.empty()) {
387    NOTREACHED();
388    return false;
389  }
390  scoped_ptr<base::ListValue> tab_values(new base::ListValue());
391  // Calculate the last |modification_time| for all entries within a window.
392  base::Time modification_time = window.timestamp;
393  for (size_t i = 0; i < window.tabs.size(); ++i) {
394    scoped_ptr<base::DictionaryValue> tab_value(new base::DictionaryValue());
395    if (SessionTabToValue(*window.tabs[i], tab_value.get())) {
396      modification_time = std::max(modification_time,
397                                   window.tabs[i]->timestamp);
398      tab_values->Append(tab_value.release());
399    }
400  }
401  if (tab_values->GetSize() == 0)
402    return false;
403  dictionary->SetString("type", "window");
404  dictionary->SetDouble("timestamp", modification_time.ToInternalValue());
405  const base::TimeDelta last_synced = base::Time::Now() - modification_time;
406  // If clock skew leads to a future time, or we last synced less than a minute
407  // ago, output "Just now".
408  dictionary->SetString("userVisibleTimestamp",
409      last_synced < base::TimeDelta::FromMinutes(1) ?
410          l10n_util::GetStringUTF16(IDS_SYNC_TIME_JUST_NOW) :
411          ui::TimeFormat::Simple(ui::TimeFormat::FORMAT_ELAPSED,
412                                 ui::TimeFormat::LENGTH_SHORT, last_synced));
413  dictionary->SetInteger("sessionId", window.window_id.id());
414  dictionary->Set("tabs", tab_values.release());
415  return true;
416}
417
418}  // namespace browser_sync
419