// Copyright (c) 2011 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/sessions/session_service.h"
#include <algorithm>
#include <limits>
#include <set>
#include <vector>
#include "base/file_util.h"
#include "base/memory/scoped_vector.h"
#include "base/message_loop.h"
#include "base/metrics/histogram.h"
#include "base/pickle.h"
#include "base/threading/thread.h"
#include "chrome/browser/extensions/extension_tab_helper.h"
#include "chrome/browser/prefs/session_startup_pref.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/sessions/session_backend.h"
#include "chrome/browser/sessions/session_command.h"
#include "chrome/browser/sessions/session_restore.h"
#include "chrome/browser/sessions/session_types.h"
#include "chrome/browser/tabs/tab_strip_model.h"
#include "chrome/browser/ui/browser_init.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/tab_contents/tab_contents_wrapper.h"
#include "chrome/common/extensions/extension.h"
#include "content/browser/tab_contents/navigation_controller.h"
#include "content/browser/tab_contents/navigation_entry.h"
#include "content/browser/tab_contents/tab_contents.h"
#include "content/common/notification_details.h"
#include "content/common/notification_service.h"
#if defined(OS_MACOSX)
#include "chrome/browser/app_controller_cppsafe_mac.h"
#endif
using base::Time;
// Identifier for commands written to file.
static const SessionCommand::id_type kCommandSetTabWindow = 0;
// kCommandSetWindowBounds is no longer used (it's superseded by
// kCommandSetWindowBounds2). I leave it here to document what it was.
// static const SessionCommand::id_type kCommandSetWindowBounds = 1;
static const SessionCommand::id_type kCommandSetTabIndexInWindow = 2;
static const SessionCommand::id_type kCommandTabClosed = 3;
static const SessionCommand::id_type kCommandWindowClosed = 4;
static const SessionCommand::id_type
kCommandTabNavigationPathPrunedFromBack = 5;
static const SessionCommand::id_type kCommandUpdateTabNavigation = 6;
static const SessionCommand::id_type kCommandSetSelectedNavigationIndex = 7;
static const SessionCommand::id_type kCommandSetSelectedTabInIndex = 8;
static const SessionCommand::id_type kCommandSetWindowType = 9;
static const SessionCommand::id_type kCommandSetWindowBounds2 = 10;
static const SessionCommand::id_type
kCommandTabNavigationPathPrunedFromFront = 11;
static const SessionCommand::id_type kCommandSetPinnedState = 12;
static const SessionCommand::id_type kCommandSetExtensionAppID = 13;
// Every kWritesPerReset commands triggers recreating the file.
static const int kWritesPerReset = 250;
namespace {
// The callback from GetLastSession is internally routed to SessionService
// first and then the caller. This is done so that the SessionWindows can be
// recreated from the SessionCommands and the SessionWindows passed to the
// caller. The following class is used for this.
class InternalSessionRequest
: public BaseSessionService::InternalGetCommandsRequest {
public:
InternalSessionRequest(
CallbackType* callback,
SessionService::SessionCallback* real_callback)
: BaseSessionService::InternalGetCommandsRequest(callback),
real_callback(real_callback) {
}
// The callback supplied to GetLastSession and GetCurrentSession.
scoped_ptr<SessionService::SessionCallback> real_callback;
private:
~InternalSessionRequest() {}
DISALLOW_COPY_AND_ASSIGN(InternalSessionRequest);
};
// Various payload structures.
struct ClosedPayload {
SessionID::id_type id;
int64 close_time;
};
struct WindowBoundsPayload2 {
SessionID::id_type window_id;
int32 x;
int32 y;
int32 w;
int32 h;
bool is_maximized;
};
struct IDAndIndexPayload {
SessionID::id_type id;
int32 index;
};
typedef IDAndIndexPayload TabIndexInWindowPayload;
typedef IDAndIndexPayload TabNavigationPathPrunedFromBackPayload;
typedef IDAndIndexPayload SelectedNavigationIndexPayload;
typedef IDAndIndexPayload SelectedTabInIndexPayload;
typedef IDAndIndexPayload WindowTypePayload;
typedef IDAndIndexPayload TabNavigationPathPrunedFromFrontPayload;
struct PinnedStatePayload {
SessionID::id_type tab_id;
bool pinned_state;
};
} // namespace
// SessionService -------------------------------------------------------------
SessionService::SessionService(Profile* profile)
: BaseSessionService(SESSION_RESTORE, profile, FilePath()),
has_open_trackable_browsers_(false),
move_on_new_browser_(false),
save_delay_in_millis_(base::TimeDelta::FromMilliseconds(2500)),
save_delay_in_mins_(base::TimeDelta::FromMinutes(10)),
save_delay_in_hrs_(base::TimeDelta::FromHours(8)) {
Init();
}
SessionService::SessionService(const FilePath& save_path)
: BaseSessionService(SESSION_RESTORE, NULL, save_path),
has_open_trackable_browsers_(false),
move_on_new_browser_(false),
save_delay_in_millis_(base::TimeDelta::FromMilliseconds(2500)),
save_delay_in_mins_(base::TimeDelta::FromMinutes(10)),
save_delay_in_hrs_(base::TimeDelta::FromHours(8)) {
Init();
}
SessionService::~SessionService() {
Save();
}
bool SessionService::RestoreIfNecessary(const std::vector<GURL>& urls_to_open) {
return RestoreIfNecessary(urls_to_open, NULL);
}
void SessionService::ResetFromCurrentBrowsers() {
ScheduleReset();
}
void SessionService::MoveCurrentSessionToLastSession() {
pending_tab_close_ids_.clear();
window_closing_ids_.clear();
pending_window_close_ids_.clear();
Save();
if (!backend_thread()) {
backend()->MoveCurrentSessionToLastSession();
} else {
backend_thread()->message_loop()->PostTask(FROM_HERE, NewRunnableMethod(
backend(), &SessionBackend::MoveCurrentSessionToLastSession));
}
}
void SessionService::SetTabWindow(const SessionID& window_id,
const SessionID& tab_id) {
if (!ShouldTrackChangesToWindow(window_id))
return;
ScheduleCommand(CreateSetTabWindowCommand(window_id, tab_id));
}
void SessionService::SetWindowBounds(const SessionID& window_id,
const gfx::Rect& bounds,
bool is_maximized) {
if (!ShouldTrackChangesToWindow(window_id))
return;
ScheduleCommand(CreateSetWindowBoundsCommand(window_id, bounds,
is_maximized));
}
void SessionService::SetTabIndexInWindow(const SessionID& window_id,
const SessionID& tab_id,
int new_index) {
if (!ShouldTrackChangesToWindow(window_id))
return;
ScheduleCommand(CreateSetTabIndexInWindowCommand(tab_id, new_index));
}
void SessionService::SetPinnedState(const SessionID& window_id,
const SessionID& tab_id,
bool is_pinned) {
if (!ShouldTrackChangesToWindow(window_id))
return;
ScheduleCommand(CreatePinnedStateCommand(tab_id, is_pinned));
}
void SessionService::TabClosed(const SessionID& window_id,
const SessionID& tab_id,
bool closed_by_user_gesture) {
if (!tab_id.id())
return; // Hapens when the tab is replaced.
if (!ShouldTrackChangesToWindow(window_id))
return;
IdToRange::iterator i = tab_to_available_range_.find(tab_id.id());
if (i != tab_to_available_range_.end())
tab_to_available_range_.erase(i);
if (find(pending_window_close_ids_.begin(), pending_window_close_ids_.end(),
window_id.id()) != pending_window_close_ids_.end()) {
// Tab is in last window. Don't commit it immediately, instead add it to the
// list of tabs to close. If the user creates another window, the close is
// committed.
pending_tab_close_ids_.insert(tab_id.id());
} else if (find(window_closing_ids_.begin(), window_closing_ids_.end(),
window_id.id()) != window_closing_ids_.end() ||
!IsOnlyOneTabLeft() ||
closed_by_user_gesture) {
// Close is the result of one of the following:
// . window close (and it isn't the last window).
// . closing a tab and there are other windows/tabs open.
// . closed by a user gesture.
// In all cases we need to mark the tab as explicitly closed.
ScheduleCommand(CreateTabClosedCommand(tab_id.id()));
} else {
// User closed the last tab in the last tabbed browser. Don't mark the
// tab closed.
pending_tab_close_ids_.insert(tab_id.id());
has_open_trackable_browsers_ = false;
}
}
void SessionService::WindowClosing(const SessionID& window_id) {
if (!ShouldTrackChangesToWindow(window_id))
return;
// The window is about to close. If there are other tabbed browsers with the
// same original profile commit the close immediately.
//
// NOTE: if the user chooses the exit menu item session service is destroyed
// and this code isn't hit.
if (has_open_trackable_browsers_) {
// Closing a window can never make has_open_trackable_browsers_ go from
// false to true, so only update it if already true.
has_open_trackable_browsers_ = HasOpenTrackableBrowsers(window_id);
}
if (should_record_close_as_pending())
pending_window_close_ids_.insert(window_id.id());
else
window_closing_ids_.insert(window_id.id());
}
void SessionService::WindowClosed(const SessionID& window_id) {
if (!ShouldTrackChangesToWindow(window_id))
return;
windows_tracking_.erase(window_id.id());
if (window_closing_ids_.find(window_id.id()) != window_closing_ids_.end()) {
window_closing_ids_.erase(window_id.id());
ScheduleCommand(CreateWindowClosedCommand(window_id.id()));
} else if (pending_window_close_ids_.find(window_id.id()) ==
pending_window_close_ids_.end()) {
// We'll hit this if user closed the last tab in a window.
has_open_trackable_browsers_ = HasOpenTrackableBrowsers(window_id);
if (should_record_close_as_pending())
pending_window_close_ids_.insert(window_id.id());
else
ScheduleCommand(CreateWindowClosedCommand(window_id.id()));
}
}
void SessionService::SetWindowType(const SessionID& window_id,
Browser::Type type) {
if (!should_track_changes_for_browser_type(type))
return;
windows_tracking_.insert(window_id.id());
// The user created a new tabbed browser with our profile. Commit any
// pending closes.
CommitPendingCloses();
has_open_trackable_browsers_ = true;
move_on_new_browser_ = true;
ScheduleCommand(
CreateSetWindowTypeCommand(window_id, WindowTypeForBrowserType(type)));
}
void SessionService::TabNavigationPathPrunedFromBack(const SessionID& window_id,
const SessionID& tab_id,
int count) {
if (!ShouldTrackChangesToWindow(window_id))
return;
TabNavigationPathPrunedFromBackPayload payload = { 0 };
payload.id = tab_id.id();
payload.index = count;
SessionCommand* command =
new SessionCommand(kCommandTabNavigationPathPrunedFromBack,
sizeof(payload));
memcpy(command->contents(), &payload, sizeof(payload));
ScheduleCommand(command);
}
void SessionService::TabNavigationPathPrunedFromFront(
const SessionID& window_id,
const SessionID& tab_id,
int count) {
if (!ShouldTrackChangesToWindow(window_id))
return;
// Update the range of indices.
if (tab_to_available_range_.find(tab_id.id()) !=
tab_to_available_range_.end()) {
std::pair<int, int>& range = tab_to_available_range_[tab_id.id()];
range.first = std::max(0, range.first - count);
range.second = std::max(0, range.second - count);
}
TabNavigationPathPrunedFromFrontPayload payload = { 0 };
payload.id = tab_id.id();
payload.index = count;
SessionCommand* command =
new SessionCommand(kCommandTabNavigationPathPrunedFromFront,
sizeof(payload));
memcpy(command->contents(), &payload, sizeof(payload));
ScheduleCommand(command);
}
void SessionService::UpdateTabNavigation(const SessionID& window_id,
const SessionID& tab_id,
int index,
const NavigationEntry& entry) {
if (!ShouldTrackEntry(entry) || !ShouldTrackChangesToWindow(window_id))
return;
if (tab_to_available_range_.find(tab_id.id()) !=
tab_to_available_range_.end()) {
std::pair<int, int>& range = tab_to_available_range_[tab_id.id()];
range.first = std::min(index, range.first);
range.second = std::max(index, range.second);
}
ScheduleCommand(CreateUpdateTabNavigationCommand(kCommandUpdateTabNavigation,
tab_id.id(), index, entry));
}
void SessionService::TabRestored(NavigationController* controller,
bool pinned) {
if (!ShouldTrackChangesToWindow(controller->window_id()))
return;
BuildCommandsForTab(controller->window_id(), controller, -1,
pinned, &pending_commands(), NULL);
StartSaveTimer();
}
void SessionService::SetSelectedNavigationIndex(const SessionID& window_id,
const SessionID& tab_id,
int index) {
if (!ShouldTrackChangesToWindow(window_id))
return;
if (tab_to_available_range_.find(tab_id.id()) !=
tab_to_available_range_.end()) {
if (index < tab_to_available_range_[tab_id.id()].first ||
index > tab_to_available_range_[tab_id.id()].second) {
// The new index is outside the range of what we've archived, schedule
// a reset.
ResetFromCurrentBrowsers();
return;
}
}
ScheduleCommand(CreateSetSelectedNavigationIndexCommand(tab_id, index));
}
void SessionService::SetSelectedTabInWindow(const SessionID& window_id,
int index) {
if (!ShouldTrackChangesToWindow(window_id))
return;
ScheduleCommand(CreateSetSelectedTabInWindow(window_id, index));
}
SessionService::Handle SessionService::GetLastSession(
CancelableRequestConsumerBase* consumer,
SessionCallback* callback) {
return ScheduleGetLastSessionCommands(
new InternalSessionRequest(
NewCallback(this, &SessionService::OnGotSessionCommands),
callback), consumer);
}
SessionService::Handle SessionService::GetCurrentSession(
CancelableRequestConsumerBase* consumer,
SessionCallback* callback) {
if (pending_window_close_ids_.empty()) {
// If there are no pending window closes, we can get the current session
// from memory.
scoped_refptr<InternalSessionRequest> request(new InternalSessionRequest(
NewCallback(this, &SessionService::OnGotSessionCommands),
callback));
AddRequest(request, consumer);
IdToRange tab_to_available_range;
std::set<SessionID::id_type> windows_to_track;
BuildCommandsFromBrowsers(&(request->commands),
&tab_to_available_range,
&windows_to_track);
request->ForwardResult(
BaseSessionService::InternalGetCommandsRequest::TupleType(
request->handle(), request));
return request->handle();
} else {
// If there are pending window closes, read the current session from disk.
return ScheduleGetCurrentSessionCommands(
new InternalSessionRequest(
NewCallback(this, &SessionService::OnGotSessionCommands),
callback), consumer);
}
}
void SessionService::Save() {
bool had_commands = !pending_commands().empty();
BaseSessionService::Save();
if (had_commands) {
RecordSessionUpdateHistogramData(NotificationType::SESSION_SERVICE_SAVED,
&last_updated_save_time_);
NotificationService::current()->Notify(
NotificationType::SESSION_SERVICE_SAVED,
Source<Profile>(profile()),
NotificationService::NoDetails());
}
}
void SessionService::Init() {
// Register for the notifications we're interested in.
registrar_.Add(this, NotificationType::TAB_PARENTED,
NotificationService::AllSources());
registrar_.Add(this, NotificationType::TAB_CLOSED,
NotificationService::AllSources());
registrar_.Add(this, NotificationType::NAV_LIST_PRUNED,
NotificationService::AllSources());
registrar_.Add(this, NotificationType::NAV_ENTRY_CHANGED,
NotificationService::AllSources());
registrar_.Add(this, NotificationType::NAV_ENTRY_COMMITTED,
NotificationService::AllSources());
registrar_.Add(this, NotificationType::BROWSER_OPENED,
NotificationService::AllSources());
registrar_.Add(this,
NotificationType::TAB_CONTENTS_APPLICATION_EXTENSION_CHANGED,
NotificationService::AllSources());
}
bool SessionService::RestoreIfNecessary(const std::vector<GURL>& urls_to_open,
Browser* browser) {
if (!has_open_trackable_browsers_ && !BrowserInit::InProcessStartup() &&
!SessionRestore::IsRestoring()
#if defined(OS_MACOSX)
// OSX has a fairly different idea of application lifetime than the
// other platforms. We need to check that we aren't opening a window
// from the dock or the menubar.
&& !app_controller_mac::IsOpeningNewWindow()
#endif
) {
// We're going from no tabbed browsers to a tabbed browser (and not in
// process startup), restore the last session.
if (move_on_new_browser_) {
// Make the current session the last.
MoveCurrentSessionToLastSession();
move_on_new_browser_ = false;
}
SessionStartupPref pref = SessionStartupPref::GetStartupPref(profile());
if (pref.type == SessionStartupPref::LAST) {
SessionRestore::RestoreSession(
profile(), browser, false, browser ? false : true, urls_to_open);
return true;
}
}
return false;
}
void SessionService::Observe(NotificationType type,
const NotificationSource& source,
const NotificationDetails& details) {
// All of our messages have the NavigationController as the source.
switch (type.value) {
case NotificationType::BROWSER_OPENED: {
Browser* browser = Source<Browser>(source).ptr();
if (browser->profile() != profile() ||
!should_track_changes_for_browser_type(browser->type())) {
return;
}
RestoreIfNecessary(std::vector<GURL>(), browser);
SetWindowType(browser->session_id(), browser->type());
break;
}
case NotificationType::TAB_PARENTED: {
NavigationController* controller =
Source<NavigationController>(source).ptr();
SetTabWindow(controller->window_id(), controller->session_id());
TabContentsWrapper* wrapper =
TabContentsWrapper::GetCurrentWrapperForContents(
controller->tab_contents());
if (wrapper->extension_tab_helper()->extension_app()) {
SetTabExtensionAppID(
controller->window_id(),
controller->session_id(),
wrapper->extension_tab_helper()->extension_app()->id());
}
break;
}
case NotificationType::TAB_CLOSED: {
NavigationController* controller =
Source<NavigationController>(source).ptr();
TabClosed(controller->window_id(), controller->session_id(),
controller->tab_contents()->closed_by_user_gesture());
RecordSessionUpdateHistogramData(NotificationType::TAB_CLOSED,
&last_updated_tab_closed_time_);
break;
}
case NotificationType::NAV_LIST_PRUNED: {
NavigationController* controller =
Source<NavigationController>(source).ptr();
Details<NavigationController::PrunedDetails> pruned_details(details);
if (pruned_details->from_front) {
TabNavigationPathPrunedFromFront(controller->window_id(),
controller->session_id(),
pruned_details->count);
} else {
TabNavigationPathPrunedFromBack(controller->window_id(),
controller->session_id(),
controller->entry_count());
}
RecordSessionUpdateHistogramData(NotificationType::NAV_LIST_PRUNED,
&last_updated_nav_list_pruned_time_);
break;
}
case NotificationType::NAV_ENTRY_CHANGED: {
NavigationController* controller =
Source<NavigationController>(source).ptr();
Details<NavigationController::EntryChangedDetails> changed(details);
UpdateTabNavigation(controller->window_id(), controller->session_id(),
changed->index, *changed->changed_entry);
break;
}
case NotificationType::NAV_ENTRY_COMMITTED: {
NavigationController* controller =
Source<NavigationController>(source).ptr();
int current_entry_index = controller->GetCurrentEntryIndex();
SetSelectedNavigationIndex(controller->window_id(),
controller->session_id(),
current_entry_index);
UpdateTabNavigation(controller->window_id(), controller->session_id(),
current_entry_index,
*controller->GetEntryAtIndex(current_entry_index));
Details<NavigationController::LoadCommittedDetails> changed(details);
if (changed->type == NavigationType::NEW_PAGE ||
changed->type == NavigationType::EXISTING_PAGE) {
RecordSessionUpdateHistogramData(NotificationType::NAV_ENTRY_COMMITTED,
&last_updated_nav_entry_commit_time_);
}
break;
}
case NotificationType::TAB_CONTENTS_APPLICATION_EXTENSION_CHANGED: {
ExtensionTabHelper* extension_tab_helper =
Source<ExtensionTabHelper>(source).ptr();
if (extension_tab_helper->extension_app()) {
SetTabExtensionAppID(
extension_tab_helper->tab_contents()->controller().window_id(),
extension_tab_helper->tab_contents()->controller().session_id(),
extension_tab_helper->extension_app()->id());
}
break;
}
default:
NOTREACHED();
}
}
void SessionService::SetTabExtensionAppID(
const SessionID& window_id,
const SessionID& tab_id,
const std::string& extension_app_id) {
if (!ShouldTrackChangesToWindow(window_id))
return;
ScheduleCommand(CreateSetTabExtensionAppIDCommand(
kCommandSetExtensionAppID,
tab_id.id(),
extension_app_id));
}
SessionCommand* SessionService::CreateSetSelectedTabInWindow(
const SessionID& window_id,
int index) {
SelectedTabInIndexPayload payload = { 0 };
payload.id = window_id.id();
payload.index = index;
SessionCommand* command = new SessionCommand(kCommandSetSelectedTabInIndex,
sizeof(payload));
memcpy(command->contents(), &payload, sizeof(payload));
return command;
}
SessionCommand* SessionService::CreateSetTabWindowCommand(
const SessionID& window_id,
const SessionID& tab_id) {
SessionID::id_type payload[] = { window_id.id(), tab_id.id() };
SessionCommand* command =
new SessionCommand(kCommandSetTabWindow, sizeof(payload));
memcpy(command->contents(), payload, sizeof(payload));
return command;
}
SessionCommand* SessionService::CreateSetWindowBoundsCommand(
const SessionID& window_id,
const gfx::Rect& bounds,
bool is_maximized) {
WindowBoundsPayload2 payload = { 0 };
payload.window_id = window_id.id();
payload.x = bounds.x();
payload.y = bounds.y();
payload.w = bounds.width();
payload.h = bounds.height();
payload.is_maximized = is_maximized;
SessionCommand* command = new SessionCommand(kCommandSetWindowBounds2,
sizeof(payload));
memcpy(command->contents(), &payload, sizeof(payload));
return command;
}
SessionCommand* SessionService::CreateSetTabIndexInWindowCommand(
const SessionID& tab_id,
int new_index) {
TabIndexInWindowPayload payload = { 0 };
payload.id = tab_id.id();
payload.index = new_index;
SessionCommand* command =
new SessionCommand(kCommandSetTabIndexInWindow, sizeof(payload));
memcpy(command->contents(), &payload, sizeof(payload));
return command;
}
SessionCommand* SessionService::CreateTabClosedCommand(
const SessionID::id_type tab_id) {
ClosedPayload payload;
// Because of what appears to be a compiler bug setting payload to {0} doesn't
// set the padding to 0, resulting in Purify reporting an UMR when we write
// the structure to disk. To avoid this we explicitly memset the struct.
memset(&payload, 0, sizeof(payload));
payload.id = tab_id;
payload.close_time = Time::Now().ToInternalValue();
SessionCommand* command =
new SessionCommand(kCommandTabClosed, sizeof(payload));
memcpy(command->contents(), &payload, sizeof(payload));
return command;
}
SessionCommand* SessionService::CreateWindowClosedCommand(
const SessionID::id_type window_id) {
ClosedPayload payload;
// See comment in CreateTabClosedCommand as to why we do this.
memset(&payload, 0, sizeof(payload));
payload.id = window_id;
payload.close_time = Time::Now().ToInternalValue();
SessionCommand* command =
new SessionCommand(kCommandWindowClosed, sizeof(payload));
memcpy(command->contents(), &payload, sizeof(payload));
return command;
}
SessionCommand* SessionService::CreateSetSelectedNavigationIndexCommand(
const SessionID& tab_id,
int index) {
SelectedNavigationIndexPayload payload = { 0 };
payload.id = tab_id.id();
payload.index = index;
SessionCommand* command = new SessionCommand(
kCommandSetSelectedNavigationIndex, sizeof(payload));
memcpy(command->contents(), &payload, sizeof(payload));
return command;
}
SessionCommand* SessionService::CreateSetWindowTypeCommand(
const SessionID& window_id,
WindowType type) {
WindowTypePayload payload = { 0 };
payload.id = window_id.id();
payload.index = static_cast<int32>(type);
SessionCommand* command = new SessionCommand(
kCommandSetWindowType, sizeof(payload));
memcpy(command->contents(), &payload, sizeof(payload));
return command;
}
SessionCommand* SessionService::CreatePinnedStateCommand(
const SessionID& tab_id,
bool is_pinned) {
PinnedStatePayload payload = { 0 };
payload.tab_id = tab_id.id();
payload.pinned_state = is_pinned;
SessionCommand* command =
new SessionCommand(kCommandSetPinnedState, sizeof(payload));
memcpy(command->contents(), &payload, sizeof(payload));
return command;
}
void SessionService::OnGotSessionCommands(
Handle handle,
scoped_refptr<InternalGetCommandsRequest> request) {
if (request->canceled())
return;
ScopedVector<SessionWindow> valid_windows;
RestoreSessionFromCommands(
request->commands, &(valid_windows.get()));
static_cast<InternalSessionRequest*>(request.get())->
real_callback->RunWithParams(
SessionCallback::TupleType(request->handle(),
&(valid_windows.get())));
}
void SessionService::RestoreSessionFromCommands(
const std::vector<SessionCommand*>& commands,
std::vector<SessionWindow*>* valid_windows) {
std::map<int, SessionTab*> tabs;
std::map<int, SessionWindow*> windows;
if (CreateTabsAndWindows(commands, &tabs, &windows)) {
AddTabsToWindows(&tabs, &windows);
SortTabsBasedOnVisualOrderAndPrune(&windows, valid_windows);
UpdateSelectedTabIndex(valid_windows);
}
STLDeleteValues(&tabs);
// Don't delete conents of windows, that is done by the caller as all
// valid windows are added to valid_windows.
}
void SessionService::UpdateSelectedTabIndex(
std::vector<SessionWindow*>* windows) {
for (std::vector<SessionWindow*>::const_iterator i = windows->begin();
i != windows->end(); ++i) {
// See note in SessionWindow as to why we do this.
int new_index = 0;
for (std::vector<SessionTab*>::const_iterator j = (*i)->tabs.begin();
j != (*i)->tabs.end(); ++j) {
if ((*j)->tab_visual_index == (*i)->selected_tab_index) {
new_index = static_cast<int>(j - (*i)->tabs.begin());
break;
}
}
(*i)->selected_tab_index = new_index;
}
}
SessionWindow* SessionService::GetWindow(
SessionID::id_type window_id,
IdToSessionWindow* windows) {
std::map<int, SessionWindow*>::iterator i = windows->find(window_id);
if (i == windows->end()) {
SessionWindow* window = new SessionWindow();
window->window_id.set_id(window_id);
(*windows)[window_id] = window;
return window;
}
return i->second;
}
SessionTab* SessionService::GetTab(
SessionID::id_type tab_id,
IdToSessionTab* tabs) {
DCHECK(tabs);
std::map<int, SessionTab*>::iterator i = tabs->find(tab_id);
if (i == tabs->end()) {
SessionTab* tab = new SessionTab();
tab->tab_id.set_id(tab_id);
(*tabs)[tab_id] = tab;
return tab;
}
return i->second;
}
std::vector<TabNavigation>::iterator
SessionService::FindClosestNavigationWithIndex(
std::vector<TabNavigation>* navigations,
int index) {
DCHECK(navigations);
for (std::vector<TabNavigation>::iterator i = navigations->begin();
i != navigations->end(); ++i) {
if (i->index() >= index)
return i;
}
return navigations->end();
}
// Function used in sorting windows. Sorting is done based on window id. As
// window ids increment for each new window, this effectively sorts by creation
// time.
static bool WindowOrderSortFunction(const SessionWindow* w1,
const SessionWindow* w2) {
return w1->window_id.id() < w2->window_id.id();
}
// Compares the two tabs based on visual index.
static bool TabVisualIndexSortFunction(const SessionTab* t1,
const SessionTab* t2) {
const int delta = t1->tab_visual_index - t2->tab_visual_index;
return delta == 0 ? (t1->tab_id.id() < t2->tab_id.id()) : (delta < 0);
}
void SessionService::SortTabsBasedOnVisualOrderAndPrune(
std::map<int, SessionWindow*>* windows,
std::vector<SessionWindow*>* valid_windows) {
std::map<int, SessionWindow*>::iterator i = windows->begin();
while (i != windows->end()) {
if (i->second->tabs.empty() || i->second->is_constrained ||
!should_track_changes_for_browser_type(
static_cast<Browser::Type>(i->second->type))) {
delete i->second;
windows->erase(i++);
} else {
// Valid window; sort the tabs and add it to the list of valid windows.
std::sort(i->second->tabs.begin(), i->second->tabs.end(),
&TabVisualIndexSortFunction);
// Add the window such that older windows appear first.
if (valid_windows->empty()) {
valid_windows->push_back(i->second);
} else {
valid_windows->insert(
std::upper_bound(valid_windows->begin(), valid_windows->end(),
i->second, &WindowOrderSortFunction),
i->second);
}
++i;
}
}
}
void SessionService::AddTabsToWindows(std::map<int, SessionTab*>* tabs,
std::map<int, SessionWindow*>* windows) {
std::map<int, SessionTab*>::iterator i = tabs->begin();
while (i != tabs->end()) {
SessionTab* tab = i->second;
if (tab->window_id.id() && !tab->navigations.empty()) {
SessionWindow* window = GetWindow(tab->window_id.id(), windows);
window->tabs.push_back(tab);
tabs->erase(i++);
// See note in SessionTab as to why we do this.
std::vector<TabNavigation>::iterator j =
FindClosestNavigationWithIndex(&(tab->navigations),
tab->current_navigation_index);
if (j == tab->navigations.end()) {
tab->current_navigation_index =
static_cast<int>(tab->navigations.size() - 1);
} else {
tab->current_navigation_index =
static_cast<int>(j - tab->navigations.begin());
}
} else {
// Never got a set tab index in window, or tabs are empty, nothing
// to do.
++i;
}
}
}
bool SessionService::CreateTabsAndWindows(
const std::vector<SessionCommand*>& data,
std::map<int, SessionTab*>* tabs,
std::map<int, SessionWindow*>* windows) {
// If the file is corrupt (command with wrong size, or unknown command), we
// still return true and attempt to restore what we we can.
for (std::vector<SessionCommand*>::const_iterator i = data.begin();
i != data.end(); ++i) {
const SessionCommand* command = *i;
switch (command->id()) {
case kCommandSetTabWindow: {
SessionID::id_type payload[2];
if (!command->GetPayload(payload, sizeof(payload)))
return true;
GetTab(payload[1], tabs)->window_id.set_id(payload[0]);
break;
}
case kCommandSetWindowBounds2: {
WindowBoundsPayload2 payload;
if (!command->GetPayload(&payload, sizeof(payload)))
return true;
GetWindow(payload.window_id, windows)->bounds.SetRect(payload.x,
payload.y,
payload.w,
payload.h);
GetWindow(payload.window_id, windows)->is_maximized =
payload.is_maximized;
break;
}
case kCommandSetTabIndexInWindow: {
TabIndexInWindowPayload payload;
if (!command->GetPayload(&payload, sizeof(payload)))
return true;
GetTab(payload.id, tabs)->tab_visual_index = payload.index;
break;
}
case kCommandTabClosed:
case kCommandWindowClosed: {
ClosedPayload payload;
if (!command->GetPayload(&payload, sizeof(payload)))
return true;
if (command->id() == kCommandTabClosed) {
delete GetTab(payload.id, tabs);
tabs->erase(payload.id);
} else {
delete GetWindow(payload.id, windows);
windows->erase(payload.id);
}
break;
}
case kCommandTabNavigationPathPrunedFromBack: {
TabNavigationPathPrunedFromBackPayload payload;
if (!command->GetPayload(&payload, sizeof(payload)))
return true;
SessionTab* tab = GetTab(payload.id, tabs);
tab->navigations.erase(
FindClosestNavigationWithIndex(&(tab->navigations), payload.index),
tab->navigations.end());
break;
}
case kCommandTabNavigationPathPrunedFromFront: {
TabNavigationPathPrunedFromFrontPayload payload;
if (!command->GetPayload(&payload, sizeof(payload)) ||
payload.index <= 0) {
return true;
}
SessionTab* tab = GetTab(payload.id, tabs);
// Update the selected navigation index.
tab->current_navigation_index =
std::max(-1, tab->current_navigation_index - payload.index);
// And update the index of existing navigations.
for (std::vector<TabNavigation>::iterator i = tab->navigations.begin();
i != tab->navigations.end();) {
i->set_index(i->index() - payload.index);
if (i->index() < 0)
i = tab->navigations.erase(i);
else
++i;
}
break;
}
case kCommandUpdateTabNavigation: {
TabNavigation navigation;
SessionID::id_type tab_id;
if (!RestoreUpdateTabNavigationCommand(*command, &navigation, &tab_id))
return true;
SessionTab* tab = GetTab(tab_id, tabs);
std::vector<TabNavigation>::iterator i =
FindClosestNavigationWithIndex(&(tab->navigations),
navigation.index());
if (i != tab->navigations.end() && i->index() == navigation.index())
*i = navigation;
else
tab->navigations.insert(i, navigation);
break;
}
case kCommandSetSelectedNavigationIndex: {
SelectedNavigationIndexPayload payload;
if (!command->GetPayload(&payload, sizeof(payload)))
return true;
GetTab(payload.id, tabs)->current_navigation_index = payload.index;
break;
}
case kCommandSetSelectedTabInIndex: {
SelectedTabInIndexPayload payload;
if (!command->GetPayload(&payload, sizeof(payload)))
return true;
GetWindow(payload.id, windows)->selected_tab_index = payload.index;
break;
}
case kCommandSetWindowType: {
WindowTypePayload payload;
if (!command->GetPayload(&payload, sizeof(payload)))
return true;
GetWindow(payload.id, windows)->is_constrained = false;
GetWindow(payload.id, windows)->type =
BrowserTypeForWindowType(
static_cast<WindowType>(payload.index));
break;
}
case kCommandSetPinnedState: {
PinnedStatePayload payload;
if (!command->GetPayload(&payload, sizeof(payload)))
return true;
GetTab(payload.tab_id, tabs)->pinned = payload.pinned_state;
break;
}
case kCommandSetExtensionAppID: {
SessionID::id_type tab_id;
std::string extension_app_id;
if (!RestoreSetTabExtensionAppIDCommand(
*command, &tab_id, &extension_app_id)) {
return true;
}
GetTab(tab_id, tabs)->extension_app_id.swap(extension_app_id);
break;
}
default:
return true;
}
}
return true;
}
void SessionService::BuildCommandsForTab(
const SessionID& window_id,
NavigationController* controller,
int index_in_window,
bool is_pinned,
std::vector<SessionCommand*>* commands,
IdToRange* tab_to_available_range) {
DCHECK(controller && commands && window_id.id());
commands->push_back(
CreateSetTabWindowCommand(window_id, controller->session_id()));
const int current_index = controller->GetCurrentEntryIndex();
const int min_index = std::max(0,
current_index - max_persist_navigation_count);
const int max_index = std::min(current_index + max_persist_navigation_count,
controller->entry_count());
const int pending_index = controller->pending_entry_index();
if (tab_to_available_range) {
(*tab_to_available_range)[controller->session_id().id()] =
std::pair<int, int>(min_index, max_index);
}
if (is_pinned) {
commands->push_back(
CreatePinnedStateCommand(controller->session_id(), true));
}
TabContentsWrapper* wrapper =
TabContentsWrapper::GetCurrentWrapperForContents(
controller->tab_contents());
if (wrapper->extension_tab_helper()->extension_app()) {
commands->push_back(
CreateSetTabExtensionAppIDCommand(
kCommandSetExtensionAppID,
controller->session_id().id(),
wrapper->extension_tab_helper()->extension_app()->id()));
}
for (int i = min_index; i < max_index; ++i) {
const NavigationEntry* entry = (i == pending_index) ?
controller->pending_entry() : controller->GetEntryAtIndex(i);
DCHECK(entry);
if (ShouldTrackEntry(*entry)) {
commands->push_back(
CreateUpdateTabNavigationCommand(kCommandUpdateTabNavigation,
controller->session_id().id(),
i,
*entry));
}
}
commands->push_back(
CreateSetSelectedNavigationIndexCommand(controller->session_id(),
current_index));
if (index_in_window != -1) {
commands->push_back(
CreateSetTabIndexInWindowCommand(controller->session_id(),
index_in_window));
}
}
void SessionService::BuildCommandsForBrowser(
Browser* browser,
std::vector<SessionCommand*>* commands,
IdToRange* tab_to_available_range,
std::set<SessionID::id_type>* windows_to_track) {
DCHECK(browser && commands);
DCHECK(browser->session_id().id());
commands->push_back(
CreateSetWindowBoundsCommand(browser->session_id(),
browser->window()->GetRestoredBounds(),
browser->window()->IsMaximized()));
commands->push_back(CreateSetWindowTypeCommand(
browser->session_id(), WindowTypeForBrowserType(browser->type())));
bool added_to_windows_to_track = false;
for (int i = 0; i < browser->tab_count(); ++i) {
TabContents* tab = browser->GetTabContentsAt(i);
DCHECK(tab);
if (tab->profile() == profile() || profile() == NULL) {
BuildCommandsForTab(browser->session_id(), &tab->controller(), i,
browser->tabstrip_model()->IsTabPinned(i),
commands, tab_to_available_range);
if (windows_to_track && !added_to_windows_to_track) {
windows_to_track->insert(browser->session_id().id());
added_to_windows_to_track = true;
}
}
}
commands->push_back(
CreateSetSelectedTabInWindow(browser->session_id(),
browser->active_index()));
}
void SessionService::BuildCommandsFromBrowsers(
std::vector<SessionCommand*>* commands,
IdToRange* tab_to_available_range,
std::set<SessionID::id_type>* windows_to_track) {
DCHECK(commands);
for (BrowserList::const_iterator i = BrowserList::begin();
i != BrowserList::end(); ++i) {
// Make sure the browser has tabs and a window. Browsers destructor
// removes itself from the BrowserList. When a browser is closed the
// destructor is not necessarily run immediately. This means its possible
// for us to get a handle to a browser that is about to be removed. If
// the tab count is 0 or the window is NULL, the browser is about to be
// deleted, so we ignore it.
if (should_track_changes_for_browser_type((*i)->type()) &&
(*i)->tab_count() && (*i)->window()) {
BuildCommandsForBrowser(*i, commands, tab_to_available_range,
windows_to_track);
}
}
}
void SessionService::ScheduleReset() {
set_pending_reset(true);
STLDeleteElements(&pending_commands());
tab_to_available_range_.clear();
windows_tracking_.clear();
BuildCommandsFromBrowsers(&pending_commands(), &tab_to_available_range_,
&windows_tracking_);
if (!windows_tracking_.empty()) {
// We're lazily created on startup and won't get an initial batch of
// SetWindowType messages. Set these here to make sure our state is correct.
has_open_trackable_browsers_ = true;
move_on_new_browser_ = true;
}
StartSaveTimer();
}
bool SessionService::ReplacePendingCommand(SessionCommand* command) {
// We only optimize page navigations, which can happen quite frequently and
// are expensive. If necessary, other commands could be searched for as
// well.
if (command->id() != kCommandUpdateTabNavigation)
return false;
void* iterator = NULL;
scoped_ptr<Pickle> command_pickle(command->PayloadAsPickle());
SessionID::id_type command_tab_id;
int command_nav_index;
if (!command_pickle->ReadInt(&iterator, &command_tab_id) ||
!command_pickle->ReadInt(&iterator, &command_nav_index)) {
return false;
}
for (std::vector<SessionCommand*>::reverse_iterator i =
pending_commands().rbegin(); i != pending_commands().rend(); ++i) {
SessionCommand* existing_command = *i;
if (existing_command->id() == kCommandUpdateTabNavigation) {
SessionID::id_type existing_tab_id;
int existing_nav_index;
{
// Creating a pickle like this means the Pickle references the data from
// the command. Make sure we delete the pickle before the command, else
// the pickle references deleted memory.
scoped_ptr<Pickle> existing_pickle(existing_command->PayloadAsPickle());
iterator = NULL;
if (!existing_pickle->ReadInt(&iterator, &existing_tab_id) ||
!existing_pickle->ReadInt(&iterator, &existing_nav_index)) {
return false;
}
}
if (existing_tab_id == command_tab_id &&
existing_nav_index == command_nav_index) {
// existing_command is an update for the same tab/index pair. Replace
// it with the new one. We need to add to the end of the list just in
// case there is a prune command after the update command.
delete existing_command;
pending_commands().erase(i.base() - 1);
pending_commands().push_back(command);
return true;
}
return false;
}
}
return false;
}
void SessionService::ScheduleCommand(SessionCommand* command) {
DCHECK(command);
if (ReplacePendingCommand(command))
return;
BaseSessionService::ScheduleCommand(command);
// Don't schedule a reset on tab closed/window closed. Otherwise we may
// lose tabs/windows we want to restore from if we exit right after this.
if (!pending_reset() && pending_window_close_ids_.empty() &&
commands_since_reset() >= kWritesPerReset &&
(command->id() != kCommandTabClosed &&
command->id() != kCommandWindowClosed)) {
ScheduleReset();
}
}
void SessionService::CommitPendingCloses() {
for (PendingTabCloseIDs::iterator i = pending_tab_close_ids_.begin();
i != pending_tab_close_ids_.end(); ++i) {
ScheduleCommand(CreateTabClosedCommand(*i));
}
pending_tab_close_ids_.clear();
for (PendingWindowCloseIDs::iterator i = pending_window_close_ids_.begin();
i != pending_window_close_ids_.end(); ++i) {
ScheduleCommand(CreateWindowClosedCommand(*i));
}
pending_window_close_ids_.clear();
}
bool SessionService::IsOnlyOneTabLeft() {
if (!profile()) {
// We're testing, always return false.
return false;
}
int window_count = 0;
for (BrowserList::const_iterator i = BrowserList::begin();
i != BrowserList::end(); ++i) {
const SessionID::id_type window_id = (*i)->session_id().id();
if (should_track_changes_for_browser_type((*i)->type()) &&
(*i)->profile() == profile() &&
window_closing_ids_.find(window_id) == window_closing_ids_.end()) {
if (++window_count > 1)
return false;
// By the time this is invoked the tab has been removed. As such, we use
// > 0 here rather than > 1.
if ((*i)->tab_count() > 0)
return false;
}
}
return true;
}
bool SessionService::HasOpenTrackableBrowsers(const SessionID& window_id) {
if (!profile()) {
// We're testing, always return false.
return true;
}
for (BrowserList::const_iterator i = BrowserList::begin();
i != BrowserList::end(); ++i) {
Browser* browser = *i;
const SessionID::id_type browser_id = browser->session_id().id();
if (browser_id != window_id.id() &&
window_closing_ids_.find(browser_id) == window_closing_ids_.end() &&
should_track_changes_for_browser_type(browser->type()) &&
browser->profile() == profile()) {
return true;
}
}
return false;
}
bool SessionService::ShouldTrackChangesToWindow(const SessionID& window_id) {
return windows_tracking_.find(window_id.id()) != windows_tracking_.end();
}
SessionService::WindowType SessionService::WindowTypeForBrowserType(
Browser::Type type) {
// We don't support masks here, only discrete types.
switch (type) {
case Browser::TYPE_POPUP:
return TYPE_POPUP;
case Browser::TYPE_APP:
return TYPE_APP;
case Browser::TYPE_APP_POPUP:
return TYPE_APP_POPUP;
case Browser::TYPE_DEVTOOLS:
return TYPE_DEVTOOLS;
case Browser::TYPE_APP_PANEL:
return TYPE_APP_PANEL;
case Browser::TYPE_NORMAL:
default:
return TYPE_NORMAL;
}
}
Browser::Type SessionService::BrowserTypeForWindowType(
SessionService::WindowType type) {
switch (type) {
case TYPE_POPUP:
return Browser::TYPE_POPUP;
case TYPE_APP:
return Browser::TYPE_APP;
case TYPE_APP_POPUP:
return Browser::TYPE_APP_POPUP;
case TYPE_DEVTOOLS:
return Browser::TYPE_DEVTOOLS;
case TYPE_APP_PANEL:
return Browser::TYPE_APP_PANEL;
case TYPE_NORMAL:
default:
return Browser::TYPE_NORMAL;
}
}
void SessionService::RecordSessionUpdateHistogramData(NotificationType type,
base::TimeTicks* last_updated_time) {
if (!last_updated_time->is_null()) {
base::TimeDelta delta = base::TimeTicks::Now() - *last_updated_time;
// We're interested in frequent updates periods longer than
// 10 minutes.
bool use_long_period = false;
if (delta >= save_delay_in_mins_) {
use_long_period = true;
}
switch (type.value) {
case NotificationType::SESSION_SERVICE_SAVED :
RecordUpdatedSaveTime(delta, use_long_period);
RecordUpdatedSessionNavigationOrTab(delta, use_long_period);
break;
case NotificationType::TAB_CLOSED:
RecordUpdatedTabClosed(delta, use_long_period);
RecordUpdatedSessionNavigationOrTab(delta, use_long_period);
break;
case NotificationType::NAV_LIST_PRUNED:
RecordUpdatedNavListPruned(delta, use_long_period);
RecordUpdatedSessionNavigationOrTab(delta, use_long_period);
break;
case NotificationType::NAV_ENTRY_COMMITTED:
RecordUpdatedNavEntryCommit(delta, use_long_period);
RecordUpdatedSessionNavigationOrTab(delta, use_long_period);
break;
default:
NOTREACHED() << "Bad type sent to RecordSessionUpdateHistogramData";
break;
}
}
(*last_updated_time) = base::TimeTicks::Now();
}
void SessionService::RecordUpdatedTabClosed(base::TimeDelta delta,
bool use_long_period) {
std::string name("SessionRestore.TabClosedPeriod");
UMA_HISTOGRAM_CUSTOM_TIMES(name,
delta,
// 2500ms is the default save delay.
save_delay_in_millis_,
save_delay_in_mins_,
50);
if (use_long_period) {
std::string long_name_("SessionRestore.TabClosedLongPeriod");
UMA_HISTOGRAM_CUSTOM_TIMES(long_name_,
delta,
save_delay_in_mins_,
save_delay_in_hrs_,
50);
}
}
void SessionService::RecordUpdatedNavListPruned(base::TimeDelta delta,
bool use_long_period) {
std::string name("SessionRestore.NavigationListPrunedPeriod");
UMA_HISTOGRAM_CUSTOM_TIMES(name,
delta,
// 2500ms is the default save delay.
save_delay_in_millis_,
save_delay_in_mins_,
50);
if (use_long_period) {
std::string long_name_("SessionRestore.NavigationListPrunedLongPeriod");
UMA_HISTOGRAM_CUSTOM_TIMES(long_name_,
delta,
save_delay_in_mins_,
save_delay_in_hrs_,
50);
}
}
void SessionService::RecordUpdatedNavEntryCommit(base::TimeDelta delta,
bool use_long_period) {
std::string name("SessionRestore.NavEntryCommittedPeriod");
UMA_HISTOGRAM_CUSTOM_TIMES(name,
delta,
// 2500ms is the default save delay.
save_delay_in_millis_,
save_delay_in_mins_,
50);
if (use_long_period) {
std::string long_name_("SessionRestore.NavEntryCommittedLongPeriod");
UMA_HISTOGRAM_CUSTOM_TIMES(long_name_,
delta,
save_delay_in_mins_,
save_delay_in_hrs_,
50);
}
}
void SessionService::RecordUpdatedSessionNavigationOrTab(base::TimeDelta delta,
bool use_long_period) {
std::string name("SessionRestore.NavOrTabUpdatePeriod");
UMA_HISTOGRAM_CUSTOM_TIMES(name,
delta,
// 2500ms is the default save delay.
save_delay_in_millis_,
save_delay_in_mins_,
50);
if (use_long_period) {
std::string long_name_("SessionRestore.NavOrTabUpdateLongPeriod");
UMA_HISTOGRAM_CUSTOM_TIMES(long_name_,
delta,
save_delay_in_mins_,
save_delay_in_hrs_,
50);
}
}
void SessionService::RecordUpdatedSaveTime(base::TimeDelta delta,
bool use_long_period) {
std::string name("SessionRestore.SavePeriod");
UMA_HISTOGRAM_CUSTOM_TIMES(name,
delta,
// 2500ms is the default save delay.
save_delay_in_millis_,
save_delay_in_mins_,
50);
if (use_long_period) {
std::string long_name_("SessionRestore.SaveLongPeriod");
UMA_HISTOGRAM_CUSTOM_TIMES(long_name_,
delta,
save_delay_in_mins_,
save_delay_in_hrs_,
50);
}
}