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