// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/ui/webui/foreign_session_handler.h" #include #include #include #include #include "base/bind.h" #include "base/bind_helpers.h" #include "base/i18n/time_formatting.h" #include "base/metrics/histogram_macros.h" #include "base/strings/string_number_conversions.h" #include "base/strings/utf_string_conversions.h" #include "base/time/time.h" #include "base/values.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/sessions/session_restore.h" #include "chrome/browser/sync/profile_sync_service_factory.h" #include "chrome/browser/ui/webui/ntp/new_tab_ui.h" #include "chrome/common/pref_names.h" #include "chrome/common/url_constants.h" #include "components/browser_sync/browser/profile_sync_service.h" #include "components/pref_registry/pref_registry_syncable.h" #include "components/prefs/pref_service.h" #include "components/prefs/scoped_user_pref_update.h" #include "components/strings/grit/components_strings.h" #include "content/public/browser/url_data_source.h" #include "content/public/browser/web_contents.h" #include "content/public/browser/web_ui.h" #include "ui/base/l10n/l10n_util.h" #include "ui/base/l10n/time_format.h" #include "ui/base/webui/web_ui_util.h" namespace browser_sync { namespace { // Maximum number of sessions we're going to display on the NTP const size_t kMaxSessionsToShow = 10; // Helper method to create JSON compatible objects from Session objects. scoped_ptr SessionTabToValue( const ::sessions::SessionTab& tab) { if (tab.navigations.empty()) return nullptr; int selected_index = std::min(tab.current_navigation_index, static_cast(tab.navigations.size() - 1)); const ::sessions::SerializedNavigationEntry& current_navigation = tab.navigations.at(selected_index); GURL tab_url = current_navigation.virtual_url(); if (!tab_url.is_valid() || tab_url.spec() == chrome::kChromeUINewTabURL) { return nullptr; } scoped_ptr dictionary(new base::DictionaryValue()); NewTabUI::SetUrlTitleAndDirection(dictionary.get(), current_navigation.title(), tab_url); dictionary->SetString("type", "tab"); dictionary->SetDouble("timestamp", static_cast(tab.timestamp.ToInternalValue())); // TODO(jeremycho): This should probably be renamed to tabId to avoid // confusion with the ID corresponding to a session. Investigate all the // places (C++ and JS) where this is being used. (http://crbug.com/154865). dictionary->SetInteger("sessionId", tab.tab_id.id()); return dictionary; } // Helper for initializing a boilerplate SessionWindow JSON compatible object. scoped_ptr BuildWindowData( base::Time modification_time, SessionID::id_type window_id) { scoped_ptr dictionary(new base::DictionaryValue()); // The items which are to be written into |dictionary| are also described in // chrome/browser/resources/ntp4/other_sessions.js in @typedef for WindowData. // Please update it whenever you add or remove any keys here. dictionary->SetString("type", "window"); dictionary->SetDouble("timestamp", modification_time.ToInternalValue()); const base::TimeDelta last_synced = base::Time::Now() - modification_time; // If clock skew leads to a future time, or we last synced less than a minute // ago, output "Just now". dictionary->SetString( "userVisibleTimestamp", last_synced < base::TimeDelta::FromMinutes(1) ? l10n_util::GetStringUTF16(IDS_SYNC_TIME_JUST_NOW) : ui::TimeFormat::Simple(ui::TimeFormat::FORMAT_ELAPSED, ui::TimeFormat::LENGTH_SHORT, last_synced)); dictionary->SetInteger("sessionId", window_id); return dictionary; } // Helper method to create JSON compatible objects from SessionWindow objects. scoped_ptr SessionWindowToValue( const ::sessions::SessionWindow& window) { if (window.tabs.empty()) return nullptr; scoped_ptr tab_values(new base::ListValue()); // Calculate the last |modification_time| for all entries within a window. base::Time modification_time = window.timestamp; for (const ::sessions::SessionTab* tab : window.tabs) { scoped_ptr tab_value(SessionTabToValue(*tab)); if (tab_value.get()) { modification_time = std::max(modification_time, tab->timestamp); tab_values->Append(tab_value.release()); } } if (tab_values->GetSize() == 0) return nullptr; scoped_ptr dictionary( BuildWindowData(window.timestamp, window.window_id.id())); dictionary->Set("tabs", tab_values.release()); return dictionary; } } // namespace ForeignSessionHandler::ForeignSessionHandler() : scoped_observer_(this) { load_attempt_time_ = base::TimeTicks::Now(); } ForeignSessionHandler::~ForeignSessionHandler() {} // static void ForeignSessionHandler::RegisterProfilePrefs( user_prefs::PrefRegistrySyncable* registry) { registry->RegisterDictionaryPref(prefs::kNtpCollapsedForeignSessions); } // static void ForeignSessionHandler::OpenForeignSessionTab( content::WebUI* web_ui, const std::string& session_string_value, SessionID::id_type window_num, SessionID::id_type tab_id, const WindowOpenDisposition& disposition) { sync_driver::OpenTabsUIDelegate* open_tabs = GetOpenTabsUIDelegate(web_ui); if (!open_tabs) return; // We don't actually care about |window_num|, this is just a sanity check. DCHECK_LT(kInvalidId, window_num); const ::sessions::SessionTab* tab; if (!open_tabs->GetForeignTab(session_string_value, tab_id, &tab)) { LOG(ERROR) << "Failed to load foreign tab."; return; } if (tab->navigations.empty()) { LOG(ERROR) << "Foreign tab no longer has valid navigations."; return; } SessionRestore::RestoreForeignSessionTab( web_ui->GetWebContents(), *tab, disposition); } // static void ForeignSessionHandler::OpenForeignSessionWindows( content::WebUI* web_ui, const std::string& session_string_value, SessionID::id_type window_num) { sync_driver::OpenTabsUIDelegate* open_tabs = GetOpenTabsUIDelegate(web_ui); if (!open_tabs) return; std::vector windows; // Note: we don't own the ForeignSessions themselves. if (!open_tabs->GetForeignSession(session_string_value, &windows)) { LOG(ERROR) << "ForeignSessionHandler failed to get session data from" "OpenTabsUIDelegate."; return; } std::vector::const_iterator iter_begin = windows.begin() + (window_num == kInvalidId ? 0 : window_num); std::vector::const_iterator iter_end = window_num == kInvalidId ? std::vector::const_iterator( windows.end()) : iter_begin + 1; SessionRestore::RestoreForeignSessionWindows(Profile::FromWebUI(web_ui), iter_begin, iter_end); } // static sync_driver::OpenTabsUIDelegate* ForeignSessionHandler::GetOpenTabsUIDelegate( content::WebUI* web_ui) { Profile* profile = Profile::FromWebUI(web_ui); ProfileSyncService* service = ProfileSyncServiceFactory::GetInstance()->GetForProfile(profile); // Only return the delegate if it exists and it is done syncing sessions. if (service && service->IsSyncActive()) return service->GetOpenTabsUIDelegate(); return NULL; } void ForeignSessionHandler::RegisterMessages() { Profile* profile = Profile::FromWebUI(web_ui()); ProfileSyncService* service = ProfileSyncServiceFactory::GetInstance()->GetForProfile(profile); // NOTE: The ProfileSyncService can be null in tests. if (service) scoped_observer_.Add(service); web_ui()->RegisterMessageCallback("deleteForeignSession", base::Bind(&ForeignSessionHandler::HandleDeleteForeignSession, base::Unretained(this))); web_ui()->RegisterMessageCallback("getForeignSessions", base::Bind(&ForeignSessionHandler::HandleGetForeignSessions, base::Unretained(this))); web_ui()->RegisterMessageCallback("openForeignSession", base::Bind(&ForeignSessionHandler::HandleOpenForeignSession, base::Unretained(this))); web_ui()->RegisterMessageCallback("setForeignSessionCollapsed", base::Bind(&ForeignSessionHandler::HandleSetForeignSessionCollapsed, base::Unretained(this))); } void ForeignSessionHandler::OnSyncConfigurationCompleted() { HandleGetForeignSessions(nullptr); } void ForeignSessionHandler::OnForeignSessionUpdated() { HandleGetForeignSessions(nullptr); } bool ForeignSessionHandler::IsTabSyncEnabled() { Profile* profile = Profile::FromWebUI(web_ui()); ProfileSyncService* service = ProfileSyncServiceFactory::GetInstance()->GetForProfile(profile); return service && service->GetActiveDataTypes().Has(syncer::PROXY_TABS); } base::string16 ForeignSessionHandler::FormatSessionTime( const base::Time& time) { // Return a time like "1 hour ago", "2 days ago", etc. base::Time now = base::Time::Now(); // TimeFormat does not support negative TimeDelta values, so then we use 0. return ui::TimeFormat::Simple( ui::TimeFormat::FORMAT_ELAPSED, ui::TimeFormat::LENGTH_SHORT, now < time ? base::TimeDelta() : now - time); } void ForeignSessionHandler::HandleGetForeignSessions( const base::ListValue* /*args*/) { sync_driver::OpenTabsUIDelegate* open_tabs = GetOpenTabsUIDelegate(web_ui()); std::vector sessions; base::ListValue session_list; if (open_tabs && open_tabs->GetAllForeignSessions(&sessions)) { if (!load_attempt_time_.is_null()) { UMA_HISTOGRAM_TIMES("Sync.SessionsRefreshDelay", base::TimeTicks::Now() - load_attempt_time_); load_attempt_time_ = base::TimeTicks(); } // Use a pref to keep track of sessions that were collapsed by the user. // To prevent the pref from accumulating stale sessions, clear it each time // and only add back sessions that are still current. DictionaryPrefUpdate pref_update(Profile::FromWebUI(web_ui())->GetPrefs(), prefs::kNtpCollapsedForeignSessions); base::DictionaryValue* current_collapsed_sessions = pref_update.Get(); scoped_ptr collapsed_sessions( current_collapsed_sessions->DeepCopy()); current_collapsed_sessions->Clear(); // Note: we don't own the SyncedSessions themselves. for (size_t i = 0; i < sessions.size() && i < kMaxSessionsToShow; ++i) { const sync_driver::SyncedSession* session = sessions[i]; const std::string& session_tag = session->session_tag; scoped_ptr session_data( new base::DictionaryValue()); // The items which are to be written into |session_data| are also // described in chrome/browser/resources/ntp4/other_sessions.js in // @typedef for SessionData. Please update it whenever you add or remove // any keys here. session_data->SetString("tag", session_tag); session_data->SetString("name", session->session_name); session_data->SetString("deviceType", session->DeviceTypeAsString()); session_data->SetString("modifiedTime", FormatSessionTime(session->modified_time)); bool is_collapsed = collapsed_sessions->HasKey(session_tag); session_data->SetBoolean("collapsed", is_collapsed); if (is_collapsed) current_collapsed_sessions->SetBoolean(session_tag, true); scoped_ptr window_list(new base::ListValue()); const std::string group_name = base::FieldTrialList::FindFullName("TabSyncByRecency"); if (group_name != "Enabled") { // Order tabs by visual order within window. for (auto map_iter : session->windows) { scoped_ptr window_data( SessionWindowToValue(*map_iter.second)); if (window_data.get()) window_list->Append(window_data.release()); } } else { // Order tabs by recency. This involves creating a synthetic singleton // window that contains all the tabs of the session. base::Time modification_time; std::vector tabs; open_tabs->GetForeignSessionTabs(session_tag, &tabs); scoped_ptr tab_values(new base::ListValue()); for (const ::sessions::SessionTab* tab : tabs) { scoped_ptr tab_value(SessionTabToValue(*tab)); if (tab_value.get()) { modification_time = std::max(modification_time, tab->timestamp); tab_values->Append(tab_value.release()); } } if (tab_values->GetSize() != 0) { scoped_ptr window_data( BuildWindowData(modification_time, 1)); window_data->Set("tabs", tab_values.release()); window_list->Append(window_data.release()); } } session_data->Set("windows", window_list.release()); session_list.Append(session_data.release()); } } base::FundamentalValue tab_sync_enabled(IsTabSyncEnabled()); web_ui()->CallJavascriptFunction("setForeignSessions", session_list, tab_sync_enabled); } void ForeignSessionHandler::HandleOpenForeignSession( const base::ListValue* args) { size_t num_args = args->GetSize(); // Expect either 1 or 8 args. For restoring an entire session, only // one argument is required -- the session tag. To restore a tab, // the additional args required are the window id, the tab id, // and 4 properties of the event object (button, altKey, ctrlKey, // metaKey, shiftKey) for determining how to open the tab. if (num_args != 8U && num_args != 1U) { LOG(ERROR) << "openForeignSession called with " << args->GetSize() << " arguments."; return; } // Extract the session tag (always provided). std::string session_string_value; if (!args->GetString(0, &session_string_value)) { LOG(ERROR) << "Failed to extract session tag."; return; } // Extract window number. std::string window_num_str; int window_num = kInvalidId; if (num_args >= 2 && (!args->GetString(1, &window_num_str) || !base::StringToInt(window_num_str, &window_num))) { LOG(ERROR) << "Failed to extract window number."; return; } // Extract tab id. std::string tab_id_str; SessionID::id_type tab_id = kInvalidId; if (num_args >= 3 && (!args->GetString(2, &tab_id_str) || !base::StringToInt(tab_id_str, &tab_id))) { LOG(ERROR) << "Failed to extract tab SessionID."; return; } if (tab_id != kInvalidId) { WindowOpenDisposition disposition = webui::GetDispositionFromClick(args, 3); OpenForeignSessionTab( web_ui(), session_string_value, window_num, tab_id, disposition); } else { OpenForeignSessionWindows(web_ui(), session_string_value, window_num); } } void ForeignSessionHandler::HandleDeleteForeignSession( const base::ListValue* args) { if (args->GetSize() != 1U) { LOG(ERROR) << "Wrong number of args to deleteForeignSession"; return; } // Get the session tag argument (required). std::string session_tag; if (!args->GetString(0, &session_tag)) { LOG(ERROR) << "Unable to extract session tag"; return; } sync_driver::OpenTabsUIDelegate* open_tabs = GetOpenTabsUIDelegate(web_ui()); if (open_tabs) open_tabs->DeleteForeignSession(session_tag); } void ForeignSessionHandler::HandleSetForeignSessionCollapsed( const base::ListValue* args) { if (args->GetSize() != 2U) { LOG(ERROR) << "Wrong number of args to setForeignSessionCollapsed"; return; } // Get the session tag argument (required). std::string session_tag; if (!args->GetString(0, &session_tag)) { LOG(ERROR) << "Unable to extract session tag"; return; } bool is_collapsed; if (!args->GetBoolean(1, &is_collapsed)) { LOG(ERROR) << "Unable to extract boolean argument"; return; } // Store session tags for collapsed sessions in a preference so that the // collapsed state persists. PrefService* prefs = Profile::FromWebUI(web_ui())->GetPrefs(); DictionaryPrefUpdate update(prefs, prefs::kNtpCollapsedForeignSessions); if (is_collapsed) update.Get()->SetBoolean(session_tag, true); else update.Get()->Remove(session_tag, NULL); } } // namespace browser_sync