Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 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> |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 10 | |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 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" |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 15 | #include "base/prefs/pref_service.h" |
| 16 | #include "base/strings/string_number_conversions.h" |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 17 | #include "base/strings/utf_string_conversions.h" |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 18 | #include "base/values.h" |
Ben Murdoch | 7dbb3d5 | 2013-07-17 14:55:54 +0100 | [diff] [blame] | 19 | #include "chrome/browser/chrome_notification_types.h" |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 20 | #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) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 25 | #include "chrome/browser/ui/host_desktop.h" |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 26 | #include "chrome/browser/ui/webui/ntp/new_tab_ui.h" |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 27 | #include "chrome/common/pref_names.h" |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 28 | #include "chrome/common/url_constants.h" |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 29 | #include "components/user_prefs/pref_registry_syncable.h" |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 30 | #include "content/public/browser/notification_service.h" |
| 31 | #include "content/public/browser/notification_source.h" |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 32 | #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) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 36 | #include "grit/generated_resources.h" |
| 37 | #include "ui/base/l10n/l10n_util.h" |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 38 | #include "ui/base/l10n/time_format.h" |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 39 | #include "ui/webui/web_ui_util.h" |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 40 | |
| 41 | namespace browser_sync { |
| 42 | |
| 43 | // Maximum number of sessions we're going to display on the NTP |
| 44 | static const size_t kMaxSessionsToShow = 10; |
| 45 | |
| 46 | namespace { |
| 47 | |
| 48 | // Comparator function for use with std::sort that will sort sessions by |
| 49 | // descending modified_time (i.e., most recent first). |
| 50 | bool SortSessionsByRecency(const SyncedSession* s1, const SyncedSession* s2) { |
| 51 | return s1->modified_time > s2->modified_time; |
| 52 | } |
| 53 | |
| 54 | } // namepace |
| 55 | |
| 56 | ForeignSessionHandler::ForeignSessionHandler() { |
| 57 | } |
| 58 | |
| 59 | // static |
Ben Murdoch | 7dbb3d5 | 2013-07-17 14:55:54 +0100 | [diff] [blame] | 60 | void ForeignSessionHandler::RegisterProfilePrefs( |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 61 | user_prefs::PrefRegistrySyncable* registry) { |
| 62 | registry->RegisterDictionaryPref( |
| 63 | prefs::kNtpCollapsedForeignSessions, |
| 64 | user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF); |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 65 | } |
| 66 | |
| 67 | // static |
| 68 | void 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 |
| 94 | void 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) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 115 | chrome::HostDesktopType host_desktop_type = |
| 116 | chrome::GetHostDesktopTypeForNativeView( |
| 117 | web_ui->GetWebContents()->GetView()->GetNativeView()); |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 118 | SessionRestore::RestoreForeignSessionWindows( |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 119 | Profile::FromWebUI(web_ui), host_desktop_type, iter_begin, iter_end); |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 120 | } |
| 121 | |
| 122 | // static |
| 123 | bool 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) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 131 | const ::sessions::SerializedNavigationEntry& current_navigation = |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 132 | 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 |
| 150 | SessionModelAssociator* 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 | |
| 163 | void 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) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 177 | } |
| 178 | |
| 179 | void 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) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 189 | } |
| 190 | |
| 191 | void 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 | |
| 213 | bool ForeignSessionHandler::IsTabSyncEnabled() { |
| 214 | Profile* profile = Profile::FromWebUI(web_ui()); |
| 215 | ProfileSyncService* service = |
| 216 | ProfileSyncServiceFactory::GetInstance()->GetForProfile(profile); |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 217 | return service && service->GetActiveDataTypes().Has(syncer::PROXY_TABS); |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 218 | } |
| 219 | |
| 220 | string16 ForeignSessionHandler::FormatSessionTime(const base::Time& time) { |
| 221 | // Return a time like "1 hour ago", "2 days ago", etc. |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 222 | base::Time now = base::Time::Now(); |
| 223 | // TimeElapsed does not support negative TimeDelta values, so then we use 0. |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 224 | return ui::TimeFormat::TimeElapsed( |
| 225 | now < time ? base::TimeDelta() : now - time); |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 226 | } |
| 227 | |
| 228 | void 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 | |
| 282 | void 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) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 321 | WindowOpenDisposition disposition = webui::GetDispositionFromClick(args, 3); |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 322 | 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 | |
| 329 | void 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 | |
| 347 | void 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) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 377 | bool 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 Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 405 | ui::TimeFormat::TimeElapsed(last_synced)); |
Torne (Richard Coles) | 5821806 | 2012-11-14 11:43:16 +0000 | [diff] [blame] | 406 | dictionary->SetInteger("sessionId", window.window_id.id()); |
| 407 | dictionary->Set("tabs", tab_values.release()); |
| 408 | return true; |
| 409 | } |
| 410 | |
| 411 | } // namespace browser_sync |