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 "components/user_prefs/pref_registry_syncable.h" 30 #include "content/public/browser/notification_service.h" 31 #include "content/public/browser/notification_source.h" 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" 36 #include "grit/generated_resources.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" 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 60 void ForeignSessionHandler::RegisterProfilePrefs( 61 user_prefs::PrefRegistrySyncable* registry) { 62 registry->RegisterDictionaryPref( 63 prefs::kNtpCollapsedForeignSessions, 64 user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF); 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 OpenTabsUIDelegate* open_tabs = GetOpenTabsUIDelegate(web_ui); 75 if (!open_tabs) 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 (!open_tabs->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 OpenTabsUIDelegate* open_tabs = GetOpenTabsUIDelegate(web_ui); 99 if (!open_tabs) 100 return; 101 102 std::vector<const SessionWindow*> windows; 103 // Note: we don't own the ForeignSessions themselves. 104 if (!open_tabs->GetForeignSession(session_string_value, &windows)) { 105 LOG(ERROR) << "ForeignSessionHandler failed to get session data from" 106 "OpenTabsUIDelegate."; 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; 115 chrome::HostDesktopType host_desktop_type = 116 chrome::GetHostDesktopTypeForNativeView( 117 web_ui->GetWebContents()->GetView()->GetNativeView()); 118 SessionRestore::RestoreForeignSessionWindows( 119 Profile::FromWebUI(web_ui), host_desktop_type, iter_begin, iter_end); 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)); 131 const ::sessions::SerializedNavigationEntry& current_navigation = 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 OpenTabsUIDelegate* ForeignSessionHandler::GetOpenTabsUIDelegate( 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 delegate if it exists and it is done syncing sessions. 157 if (service && service->ShouldPushChanges()) 158 return service->GetOpenTabsUIDelegate(); 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))); 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)); 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); 217 return service && service->GetActiveDataTypes().Has(syncer::PROXY_TABS); 218 } 219 220 base::string16 ForeignSessionHandler::FormatSessionTime( 221 const base::Time& time) { 222 // Return a time like "1 hour ago", "2 days ago", etc. 223 base::Time now = base::Time::Now(); 224 // TimeElapsed does not support negative TimeDelta values, so then we use 0. 225 return ui::TimeFormat::TimeElapsed( 226 now < time ? base::TimeDelta() : now - time); 227 } 228 229 void ForeignSessionHandler::HandleGetForeignSessions(const ListValue* args) { 230 OpenTabsUIDelegate* open_tabs = GetOpenTabsUIDelegate(web_ui()); 231 std::vector<const SyncedSession*> sessions; 232 233 ListValue session_list; 234 if (open_tabs && open_tabs->GetAllForeignSessions(&sessions)) { 235 // Sort sessions from most recent to least recent. 236 std::sort(sessions.begin(), sessions.end(), SortSessionsByRecency); 237 238 // Use a pref to keep track of sessions that were collapsed by the user. 239 // To prevent the pref from accumulating stale sessions, clear it each time 240 // and only add back sessions that are still current. 241 DictionaryPrefUpdate pref_update(Profile::FromWebUI(web_ui())->GetPrefs(), 242 prefs::kNtpCollapsedForeignSessions); 243 DictionaryValue* current_collapsed_sessions = pref_update.Get(); 244 scoped_ptr<DictionaryValue> collapsed_sessions( 245 current_collapsed_sessions->DeepCopy()); 246 current_collapsed_sessions->Clear(); 247 248 // Note: we don't own the SyncedSessions themselves. 249 for (size_t i = 0; i < sessions.size() && i < kMaxSessionsToShow; ++i) { 250 const SyncedSession* session = sessions[i]; 251 const std::string& session_tag = session->session_tag; 252 scoped_ptr<DictionaryValue> session_data(new DictionaryValue()); 253 session_data->SetString("tag", session_tag); 254 session_data->SetString("name", session->session_name); 255 session_data->SetString("deviceType", session->DeviceTypeAsString()); 256 session_data->SetString("modifiedTime", 257 FormatSessionTime(session->modified_time)); 258 259 bool is_collapsed = collapsed_sessions->HasKey(session_tag); 260 session_data->SetBoolean("collapsed", is_collapsed); 261 if (is_collapsed) 262 current_collapsed_sessions->SetBoolean(session_tag, true); 263 264 scoped_ptr<ListValue> window_list(new ListValue()); 265 for (SyncedSession::SyncedWindowMap::const_iterator it = 266 session->windows.begin(); it != session->windows.end(); ++it) { 267 SessionWindow* window = it->second; 268 scoped_ptr<DictionaryValue> window_data(new DictionaryValue()); 269 if (SessionWindowToValue(*window, window_data.get())) 270 window_list->Append(window_data.release()); 271 } 272 273 session_data->Set("windows", window_list.release()); 274 session_list.Append(session_data.release()); 275 } 276 } 277 base::FundamentalValue tab_sync_enabled(IsTabSyncEnabled()); 278 web_ui()->CallJavascriptFunction("ntp.setForeignSessions", 279 session_list, 280 tab_sync_enabled); 281 } 282 283 void ForeignSessionHandler::HandleOpenForeignSession(const ListValue* args) { 284 size_t num_args = args->GetSize(); 285 // Expect either 1 or 8 args. For restoring an entire session, only 286 // one argument is required -- the session tag. To restore a tab, 287 // the additional args required are the window id, the tab id, 288 // and 4 properties of the event object (button, altKey, ctrlKey, 289 // metaKey, shiftKey) for determining how to open the tab. 290 if (num_args != 8U && num_args != 1U) { 291 LOG(ERROR) << "openForeignSession called with " << args->GetSize() 292 << " arguments."; 293 return; 294 } 295 296 // Extract the session tag (always provided). 297 std::string session_string_value; 298 if (!args->GetString(0, &session_string_value)) { 299 LOG(ERROR) << "Failed to extract session tag."; 300 return; 301 } 302 303 // Extract window number. 304 std::string window_num_str; 305 int window_num = kInvalidId; 306 if (num_args >= 2 && (!args->GetString(1, &window_num_str) || 307 !base::StringToInt(window_num_str, &window_num))) { 308 LOG(ERROR) << "Failed to extract window number."; 309 return; 310 } 311 312 // Extract tab id. 313 std::string tab_id_str; 314 SessionID::id_type tab_id = kInvalidId; 315 if (num_args >= 3 && (!args->GetString(2, &tab_id_str) || 316 !base::StringToInt(tab_id_str, &tab_id))) { 317 LOG(ERROR) << "Failed to extract tab SessionID."; 318 return; 319 } 320 321 if (tab_id != kInvalidId) { 322 WindowOpenDisposition disposition = webui::GetDispositionFromClick(args, 3); 323 OpenForeignSessionTab( 324 web_ui(), session_string_value, window_num, tab_id, disposition); 325 } else { 326 OpenForeignSessionWindows(web_ui(), session_string_value, window_num); 327 } 328 } 329 330 void ForeignSessionHandler::HandleDeleteForeignSession(const ListValue* args) { 331 if (args->GetSize() != 1U) { 332 LOG(ERROR) << "Wrong number of args to deleteForeignSession"; 333 return; 334 } 335 336 // Get the session tag argument (required). 337 std::string session_tag; 338 if (!args->GetString(0, &session_tag)) { 339 LOG(ERROR) << "Unable to extract session tag"; 340 return; 341 } 342 343 OpenTabsUIDelegate* open_tabs = GetOpenTabsUIDelegate(web_ui()); 344 if (open_tabs) 345 open_tabs->DeleteForeignSession(session_tag); 346 } 347 348 void ForeignSessionHandler::HandleSetForeignSessionCollapsed( 349 const ListValue* args) { 350 if (args->GetSize() != 2U) { 351 LOG(ERROR) << "Wrong number of args to setForeignSessionCollapsed"; 352 return; 353 } 354 355 // Get the session tag argument (required). 356 std::string session_tag; 357 if (!args->GetString(0, &session_tag)) { 358 LOG(ERROR) << "Unable to extract session tag"; 359 return; 360 } 361 362 bool is_collapsed; 363 if (!args->GetBoolean(1, &is_collapsed)) { 364 LOG(ERROR) << "Unable to extract boolean argument"; 365 return; 366 } 367 368 // Store session tags for collapsed sessions in a preference so that the 369 // collapsed state persists. 370 PrefService* prefs = Profile::FromWebUI(web_ui())->GetPrefs(); 371 DictionaryPrefUpdate update(prefs, prefs::kNtpCollapsedForeignSessions); 372 if (is_collapsed) 373 update.Get()->SetBoolean(session_tag, true); 374 else 375 update.Get()->Remove(session_tag, NULL); 376 } 377 378 bool ForeignSessionHandler::SessionWindowToValue( 379 const SessionWindow& window, 380 DictionaryValue* dictionary) { 381 if (window.tabs.empty()) { 382 NOTREACHED(); 383 return false; 384 } 385 scoped_ptr<ListValue> tab_values(new ListValue()); 386 // Calculate the last |modification_time| for all entries within a window. 387 base::Time modification_time = window.timestamp; 388 for (size_t i = 0; i < window.tabs.size(); ++i) { 389 scoped_ptr<DictionaryValue> tab_value(new DictionaryValue()); 390 if (SessionTabToValue(*window.tabs[i], tab_value.get())) { 391 modification_time = std::max(modification_time, 392 window.tabs[i]->timestamp); 393 tab_values->Append(tab_value.release()); 394 } 395 } 396 if (tab_values->GetSize() == 0) 397 return false; 398 dictionary->SetString("type", "window"); 399 dictionary->SetDouble("timestamp", modification_time.ToInternalValue()); 400 const base::TimeDelta last_synced = base::Time::Now() - modification_time; 401 // If clock skew leads to a future time, or we last synced less than a minute 402 // ago, output "Just now". 403 dictionary->SetString("userVisibleTimestamp", 404 last_synced < base::TimeDelta::FromMinutes(1) ? 405 l10n_util::GetStringUTF16(IDS_SYNC_TIME_JUST_NOW) : 406 ui::TimeFormat::TimeElapsed(last_synced)); 407 dictionary->SetInteger("sessionId", window.window_id.id()); 408 dictionary->Set("tabs", tab_values.release()); 409 return true; 410 } 411 412 } // namespace browser_sync 413