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.
5 #include "chrome/browser/ui/webui/foreign_session_handler.h"
11 #include "base/bind.h"
12 #include "base/bind_helpers.h"
13 #include "base/i18n/time_formatting.h"
14 #include "base/metrics/histogram_macros.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/time/time.h"
20 #include "base/values.h"
21 #include "chrome/browser/chrome_notification_types.h"
22 #include "chrome/browser/profiles/profile.h"
23 #include "chrome/browser/sessions/session_restore.h"
24 #include "chrome/browser/sync/profile_sync_service.h"
25 #include "chrome/browser/sync/profile_sync_service_factory.h"
26 #include "chrome/browser/ui/host_desktop.h"
27 #include "chrome/browser/ui/webui/ntp/new_tab_ui.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/pref_registry/pref_registry_syncable.h"
32 #include "content/public/browser/notification_service.h"
33 #include "content/public/browser/notification_source.h"
34 #include "content/public/browser/url_data_source.h"
35 #include "content/public/browser/web_contents.h"
36 #include "content/public/browser/web_ui.h"
37 #include "ui/base/l10n/l10n_util.h"
38 #include "ui/base/l10n/time_format.h"
39 #include "ui/base/webui/web_ui_util.h"
41 namespace browser_sync
{
45 // Maximum number of sessions we're going to display on the NTP
46 const size_t kMaxSessionsToShow
= 10;
48 // Helper method to create JSON compatible objects from Session objects.
49 scoped_ptr
<base::DictionaryValue
> SessionTabToValue(
50 const ::sessions::SessionTab
& tab
) {
51 if (tab
.navigations
.empty())
54 int selected_index
= std::min(tab
.current_navigation_index
,
55 static_cast<int>(tab
.navigations
.size() - 1));
56 const ::sessions::SerializedNavigationEntry
& current_navigation
=
57 tab
.navigations
.at(selected_index
);
58 GURL tab_url
= current_navigation
.virtual_url();
59 if (!tab_url
.is_valid() ||
60 tab_url
.spec() == chrome::kChromeUINewTabURL
) {
64 scoped_ptr
<base::DictionaryValue
> dictionary(new base::DictionaryValue());
65 NewTabUI::SetUrlTitleAndDirection(dictionary
.get(),
66 current_navigation
.title(), tab_url
);
67 dictionary
->SetString("type", "tab");
68 dictionary
->SetDouble("timestamp",
69 static_cast<double>(tab
.timestamp
.ToInternalValue()));
70 // TODO(jeremycho): This should probably be renamed to tabId to avoid
71 // confusion with the ID corresponding to a session. Investigate all the
72 // places (C++ and JS) where this is being used. (http://crbug.com/154865).
73 dictionary
->SetInteger("sessionId", tab
.tab_id
.id());
74 return dictionary
.Pass();
77 // Helper for initializing a boilerplate SessionWindow JSON compatible object.
78 scoped_ptr
<base::DictionaryValue
> BuildWindowData(
79 base::Time modification_time
,
80 SessionID::id_type window_id
) {
81 scoped_ptr
<base::DictionaryValue
> dictionary(new base::DictionaryValue());
82 // The items which are to be written into |dictionary| are also described in
83 // chrome/browser/resources/ntp4/other_sessions.js in @typedef for WindowData.
84 // Please update it whenever you add or remove any keys here.
85 dictionary
->SetString("type", "window");
86 dictionary
->SetDouble("timestamp", modification_time
.ToInternalValue());
87 const base::TimeDelta last_synced
= base::Time::Now() - modification_time
;
88 // If clock skew leads to a future time, or we last synced less than a minute
89 // ago, output "Just now".
90 dictionary
->SetString(
91 "userVisibleTimestamp",
92 last_synced
< base::TimeDelta::FromMinutes(1)
93 ? l10n_util::GetStringUTF16(IDS_SYNC_TIME_JUST_NOW
)
94 : ui::TimeFormat::Simple(ui::TimeFormat::FORMAT_ELAPSED
,
95 ui::TimeFormat::LENGTH_SHORT
, last_synced
));
97 dictionary
->SetInteger("sessionId", window_id
);
98 return dictionary
.Pass();
101 // Helper method to create JSON compatible objects from SessionWindow objects.
102 scoped_ptr
<base::DictionaryValue
> SessionWindowToValue(
103 const ::sessions::SessionWindow
& window
) {
104 if (window
.tabs
.empty())
106 scoped_ptr
<base::ListValue
> tab_values(new base::ListValue());
107 // Calculate the last |modification_time| for all entries within a window.
108 base::Time modification_time
= window
.timestamp
;
109 for (const ::sessions::SessionTab
* tab
: window
.tabs
) {
110 scoped_ptr
<base::DictionaryValue
> tab_value(SessionTabToValue(*tab
));
111 if (tab_value
.get()) {
112 modification_time
= std::max(modification_time
,
114 tab_values
->Append(tab_value
.release());
117 if (tab_values
->GetSize() == 0)
119 scoped_ptr
<base::DictionaryValue
> dictionary(
120 BuildWindowData(window
.timestamp
, window
.window_id
.id()));
121 dictionary
->Set("tabs", tab_values
.release());
122 return dictionary
.Pass();
127 ForeignSessionHandler::ForeignSessionHandler() {
128 load_attempt_time_
= base::TimeTicks::Now();
132 void ForeignSessionHandler::RegisterProfilePrefs(
133 user_prefs::PrefRegistrySyncable
* registry
) {
134 registry
->RegisterDictionaryPref(prefs::kNtpCollapsedForeignSessions
);
138 void ForeignSessionHandler::OpenForeignSessionTab(
139 content::WebUI
* web_ui
,
140 const std::string
& session_string_value
,
141 SessionID::id_type window_num
,
142 SessionID::id_type tab_id
,
143 const WindowOpenDisposition
& disposition
) {
144 sync_driver::OpenTabsUIDelegate
* open_tabs
= GetOpenTabsUIDelegate(web_ui
);
148 // We don't actually care about |window_num|, this is just a sanity check.
149 DCHECK_LT(kInvalidId
, window_num
);
150 const ::sessions::SessionTab
* tab
;
151 if (!open_tabs
->GetForeignTab(session_string_value
, tab_id
, &tab
)) {
152 LOG(ERROR
) << "Failed to load foreign tab.";
155 if (tab
->navigations
.empty()) {
156 LOG(ERROR
) << "Foreign tab no longer has valid navigations.";
159 SessionRestore::RestoreForeignSessionTab(
160 web_ui
->GetWebContents(), *tab
, disposition
);
164 void ForeignSessionHandler::OpenForeignSessionWindows(
165 content::WebUI
* web_ui
,
166 const std::string
& session_string_value
,
167 SessionID::id_type window_num
) {
168 sync_driver::OpenTabsUIDelegate
* open_tabs
= GetOpenTabsUIDelegate(web_ui
);
172 std::vector
<const ::sessions::SessionWindow
*> windows
;
173 // Note: we don't own the ForeignSessions themselves.
174 if (!open_tabs
->GetForeignSession(session_string_value
, &windows
)) {
175 LOG(ERROR
) << "ForeignSessionHandler failed to get session data from"
176 "OpenTabsUIDelegate.";
179 std::vector
<const ::sessions::SessionWindow
*>::const_iterator iter_begin
=
180 windows
.begin() + (window_num
== kInvalidId
? 0 : window_num
);
181 std::vector
<const ::sessions::SessionWindow
*>::const_iterator iter_end
=
182 window_num
== kInvalidId
?
183 std::vector
<const ::sessions::SessionWindow
*>::const_iterator(
184 windows
.end()) : iter_begin
+ 1;
185 chrome::HostDesktopType host_desktop_type
=
186 chrome::GetHostDesktopTypeForNativeView(
187 web_ui
->GetWebContents()->GetNativeView());
188 SessionRestore::RestoreForeignSessionWindows(
189 Profile::FromWebUI(web_ui
), host_desktop_type
, iter_begin
, iter_end
);
193 sync_driver::OpenTabsUIDelegate
* ForeignSessionHandler::GetOpenTabsUIDelegate(
194 content::WebUI
* web_ui
) {
195 Profile
* profile
= Profile::FromWebUI(web_ui
);
196 ProfileSyncService
* service
=
197 ProfileSyncServiceFactory::GetInstance()->GetForProfile(profile
);
199 // Only return the delegate if it exists and it is done syncing sessions.
200 if (service
&& service
->IsSyncActive())
201 return service
->GetOpenTabsUIDelegate();
206 void ForeignSessionHandler::RegisterMessages() {
207 Profile
* profile
= Profile::FromWebUI(web_ui());
208 ProfileSyncService
* service
=
209 ProfileSyncServiceFactory::GetInstance()->GetForProfile(profile
);
210 registrar_
.Add(this, chrome::NOTIFICATION_SYNC_CONFIGURE_DONE
,
211 content::Source
<ProfileSyncService
>(service
));
212 registrar_
.Add(this, chrome::NOTIFICATION_FOREIGN_SESSION_UPDATED
,
213 content::Source
<Profile
>(profile
));
214 registrar_
.Add(this, chrome::NOTIFICATION_FOREIGN_SESSION_DISABLED
,
215 content::Source
<Profile
>(profile
));
217 web_ui()->RegisterMessageCallback("deleteForeignSession",
218 base::Bind(&ForeignSessionHandler::HandleDeleteForeignSession
,
219 base::Unretained(this)));
220 web_ui()->RegisterMessageCallback("getForeignSessions",
221 base::Bind(&ForeignSessionHandler::HandleGetForeignSessions
,
222 base::Unretained(this)));
223 web_ui()->RegisterMessageCallback("openForeignSession",
224 base::Bind(&ForeignSessionHandler::HandleOpenForeignSession
,
225 base::Unretained(this)));
226 web_ui()->RegisterMessageCallback("setForeignSessionCollapsed",
227 base::Bind(&ForeignSessionHandler::HandleSetForeignSessionCollapsed
,
228 base::Unretained(this)));
231 void ForeignSessionHandler::Observe(
233 const content::NotificationSource
& source
,
234 const content::NotificationDetails
& details
) {
236 case chrome::NOTIFICATION_FOREIGN_SESSION_DISABLED
:
237 // Tab sync is disabled, so clean up data about collapsed sessions.
238 Profile::FromWebUI(web_ui())->GetPrefs()->ClearPref(
239 prefs::kNtpCollapsedForeignSessions
);
241 case chrome::NOTIFICATION_SYNC_CONFIGURE_DONE
:
242 case chrome::NOTIFICATION_FOREIGN_SESSION_UPDATED
:
243 HandleGetForeignSessions(nullptr);
250 bool ForeignSessionHandler::IsTabSyncEnabled() {
251 Profile
* profile
= Profile::FromWebUI(web_ui());
252 ProfileSyncService
* service
=
253 ProfileSyncServiceFactory::GetInstance()->GetForProfile(profile
);
254 return service
&& service
->GetActiveDataTypes().Has(syncer::PROXY_TABS
);
257 base::string16
ForeignSessionHandler::FormatSessionTime(
258 const base::Time
& time
) {
259 // Return a time like "1 hour ago", "2 days ago", etc.
260 base::Time now
= base::Time::Now();
261 // TimeFormat does not support negative TimeDelta values, so then we use 0.
262 return ui::TimeFormat::Simple(
263 ui::TimeFormat::FORMAT_ELAPSED
, ui::TimeFormat::LENGTH_SHORT
,
264 now
< time
? base::TimeDelta() : now
- time
);
267 void ForeignSessionHandler::HandleGetForeignSessions(
268 const base::ListValue
* /*args*/) {
269 sync_driver::OpenTabsUIDelegate
* open_tabs
= GetOpenTabsUIDelegate(web_ui());
270 std::vector
<const sync_driver::SyncedSession
*> sessions
;
272 base::ListValue session_list
;
273 if (open_tabs
&& open_tabs
->GetAllForeignSessions(&sessions
)) {
274 if (!load_attempt_time_
.is_null()) {
275 UMA_HISTOGRAM_TIMES("Sync.SessionsRefreshDelay",
276 base::TimeTicks::Now() - load_attempt_time_
);
277 load_attempt_time_
= base::TimeTicks();
280 // Use a pref to keep track of sessions that were collapsed by the user.
281 // To prevent the pref from accumulating stale sessions, clear it each time
282 // and only add back sessions that are still current.
283 DictionaryPrefUpdate
pref_update(Profile::FromWebUI(web_ui())->GetPrefs(),
284 prefs::kNtpCollapsedForeignSessions
);
285 base::DictionaryValue
* current_collapsed_sessions
= pref_update
.Get();
286 scoped_ptr
<base::DictionaryValue
> collapsed_sessions(
287 current_collapsed_sessions
->DeepCopy());
288 current_collapsed_sessions
->Clear();
290 // Note: we don't own the SyncedSessions themselves.
291 for (size_t i
= 0; i
< sessions
.size() && i
< kMaxSessionsToShow
; ++i
) {
292 const sync_driver::SyncedSession
* session
= sessions
[i
];
293 const std::string
& session_tag
= session
->session_tag
;
294 scoped_ptr
<base::DictionaryValue
> session_data(
295 new base::DictionaryValue());
296 // The items which are to be written into |session_data| are also
297 // described in chrome/browser/resources/ntp4/other_sessions.js in
298 // @typedef for SessionData. Please update it whenever you add or remove
300 session_data
->SetString("tag", session_tag
);
301 session_data
->SetString("name", session
->session_name
);
302 session_data
->SetString("deviceType", session
->DeviceTypeAsString());
303 session_data
->SetString("modifiedTime",
304 FormatSessionTime(session
->modified_time
));
306 bool is_collapsed
= collapsed_sessions
->HasKey(session_tag
);
307 session_data
->SetBoolean("collapsed", is_collapsed
);
309 current_collapsed_sessions
->SetBoolean(session_tag
, true);
311 scoped_ptr
<base::ListValue
> window_list(new base::ListValue());
312 const std::string group_name
=
313 base::FieldTrialList::FindFullName("TabSyncByRecency");
314 if (group_name
!= "Enabled") {
315 // Order tabs by visual order within window.
316 for (auto map_iter
: session
->windows
) {
317 scoped_ptr
<base::DictionaryValue
> window_data(
318 SessionWindowToValue(*map_iter
.second
));
319 if (window_data
.get())
320 window_list
->Append(window_data
.release());
323 // Order tabs by recency. This involves creating a synthetic singleton
324 // window that contains all the tabs of the session.
325 base::Time modification_time
;
326 std::vector
<const ::sessions::SessionTab
*> tabs
;
327 open_tabs
->GetForeignSessionTabs(session_tag
, &tabs
);
328 scoped_ptr
<base::ListValue
> tab_values(new base::ListValue());
329 for (const ::sessions::SessionTab
* tab
: tabs
) {
330 scoped_ptr
<base::DictionaryValue
> tab_value(SessionTabToValue(*tab
));
331 if (tab_value
.get()) {
332 modification_time
= std::max(modification_time
, tab
->timestamp
);
333 tab_values
->Append(tab_value
.release());
336 if (tab_values
->GetSize() != 0) {
337 scoped_ptr
<base::DictionaryValue
> window_data(
338 BuildWindowData(modification_time
, 1));
339 window_data
->Set("tabs", tab_values
.release());
340 window_list
->Append(window_data
.release());
344 session_data
->Set("windows", window_list
.release());
345 session_list
.Append(session_data
.release());
348 base::FundamentalValue
tab_sync_enabled(IsTabSyncEnabled());
349 web_ui()->CallJavascriptFunction("setForeignSessions", session_list
,
353 void ForeignSessionHandler::HandleOpenForeignSession(
354 const base::ListValue
* args
) {
355 size_t num_args
= args
->GetSize();
356 // Expect either 1 or 8 args. For restoring an entire session, only
357 // one argument is required -- the session tag. To restore a tab,
358 // the additional args required are the window id, the tab id,
359 // and 4 properties of the event object (button, altKey, ctrlKey,
360 // metaKey, shiftKey) for determining how to open the tab.
361 if (num_args
!= 8U && num_args
!= 1U) {
362 LOG(ERROR
) << "openForeignSession called with " << args
->GetSize()
367 // Extract the session tag (always provided).
368 std::string session_string_value
;
369 if (!args
->GetString(0, &session_string_value
)) {
370 LOG(ERROR
) << "Failed to extract session tag.";
374 // Extract window number.
375 std::string window_num_str
;
376 int window_num
= kInvalidId
;
377 if (num_args
>= 2 && (!args
->GetString(1, &window_num_str
) ||
378 !base::StringToInt(window_num_str
, &window_num
))) {
379 LOG(ERROR
) << "Failed to extract window number.";
384 std::string tab_id_str
;
385 SessionID::id_type tab_id
= kInvalidId
;
386 if (num_args
>= 3 && (!args
->GetString(2, &tab_id_str
) ||
387 !base::StringToInt(tab_id_str
, &tab_id
))) {
388 LOG(ERROR
) << "Failed to extract tab SessionID.";
392 if (tab_id
!= kInvalidId
) {
393 WindowOpenDisposition disposition
= webui::GetDispositionFromClick(args
, 3);
394 OpenForeignSessionTab(
395 web_ui(), session_string_value
, window_num
, tab_id
, disposition
);
397 OpenForeignSessionWindows(web_ui(), session_string_value
, window_num
);
401 void ForeignSessionHandler::HandleDeleteForeignSession(
402 const base::ListValue
* args
) {
403 if (args
->GetSize() != 1U) {
404 LOG(ERROR
) << "Wrong number of args to deleteForeignSession";
408 // Get the session tag argument (required).
409 std::string session_tag
;
410 if (!args
->GetString(0, &session_tag
)) {
411 LOG(ERROR
) << "Unable to extract session tag";
415 sync_driver::OpenTabsUIDelegate
* open_tabs
= GetOpenTabsUIDelegate(web_ui());
417 open_tabs
->DeleteForeignSession(session_tag
);
420 void ForeignSessionHandler::HandleSetForeignSessionCollapsed(
421 const base::ListValue
* args
) {
422 if (args
->GetSize() != 2U) {
423 LOG(ERROR
) << "Wrong number of args to setForeignSessionCollapsed";
427 // Get the session tag argument (required).
428 std::string session_tag
;
429 if (!args
->GetString(0, &session_tag
)) {
430 LOG(ERROR
) << "Unable to extract session tag";
435 if (!args
->GetBoolean(1, &is_collapsed
)) {
436 LOG(ERROR
) << "Unable to extract boolean argument";
440 // Store session tags for collapsed sessions in a preference so that the
441 // collapsed state persists.
442 PrefService
* prefs
= Profile::FromWebUI(web_ui())->GetPrefs();
443 DictionaryPrefUpdate
update(prefs
, prefs::kNtpCollapsedForeignSessions
);
445 update
.Get()->SetBoolean(session_tag
, true);
447 update
.Get()->Remove(session_tag
, NULL
);
450 } // namespace browser_sync