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