blob: 2fc93f48c6a9fc268973f9e1517f125b00b1016f [file] [log] [blame]
// 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 <stddef.h>
#include <algorithm>
#include <string>
#include <vector>
#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.
std::unique_ptr<base::DictionaryValue> SessionTabToValue(
const ::sessions::SessionTab& tab) {
if (tab.navigations.empty())
return nullptr;
int selected_index = std::min(tab.current_navigation_index,
static_cast<int>(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;
}
std::unique_ptr<base::DictionaryValue> dictionary(
new base::DictionaryValue());
NewTabUI::SetUrlTitleAndDirection(dictionary.get(),
current_navigation.title(), tab_url);
dictionary->SetString("type", "tab");
dictionary->SetDouble("timestamp",
static_cast<double>(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.
std::unique_ptr<base::DictionaryValue> BuildWindowData(
base::Time modification_time,
SessionID::id_type window_id) {
std::unique_ptr<base::DictionaryValue> 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.
std::unique_ptr<base::DictionaryValue> SessionWindowToValue(
const ::sessions::SessionWindow& window) {
if (window.tabs.empty())
return nullptr;
std::unique_ptr<base::ListValue> 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) {
std::unique_ptr<base::DictionaryValue> 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;
std::unique_ptr<base::DictionaryValue> 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<const ::sessions::SessionWindow*> 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 ::sessions::SessionWindow*>::const_iterator iter_begin =
windows.begin() + (window_num == kInvalidId ? 0 : window_num);
std::vector<const ::sessions::SessionWindow*>::const_iterator iter_end =
window_num == kInvalidId ?
std::vector<const ::sessions::SessionWindow*>::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<const sync_driver::SyncedSession*> 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();
std::unique_ptr<base::DictionaryValue> 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;
std::unique_ptr<base::DictionaryValue> session_data(
new base::DictionaryValue());
// The items which are to be written into |session_data| are also
// described in chrome/browser/resources/history/externs.js
// @typedef for ForeignSession. 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));
session_data->SetDouble("timestamp", session->modified_time.ToJsTime());
bool is_collapsed = collapsed_sessions->HasKey(session_tag);
session_data->SetBoolean("collapsed", is_collapsed);
if (is_collapsed)
current_collapsed_sessions->SetBoolean(session_tag, true);
std::unique_ptr<base::ListValue> 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) {
std::unique_ptr<base::DictionaryValue> 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<const ::sessions::SessionTab*> tabs;
open_tabs->GetForeignSessionTabs(session_tag, &tabs);
std::unique_ptr<base::ListValue> tab_values(new base::ListValue());
for (const ::sessions::SessionTab* tab : tabs) {
std::unique_ptr<base::DictionaryValue> 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) {
std::unique_ptr<base::DictionaryValue> 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()->CallJavascriptFunctionUnsafe("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