// Copyright 2012 The Chromium Authors
// 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 <stddef.h>

#include <memory>
#include <set>
#include <string>
#include <utility>
#include <vector>

#include "base/command_line.h"
#include "base/debug/alias.h"
#include "base/debug/dump_without_crashing.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/apps/app_service/launch_utils.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/buildflags.h"
#include "chrome/browser/lifetime/application_lifetime_desktop.h"
#include "chrome/browser/lifetime/browser_shutdown.h"
#include "chrome/browser/prefs/session_startup_pref.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_attributes_entry.h"
#include "chrome/browser/profiles/profile_attributes_storage.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/profiles/profiles_state.h"
#include "chrome/browser/sessions/exit_type_service.h"
#include "chrome/browser/sessions/session_common_utils.h"
#include "chrome/browser/sessions/session_data_deleter.h"
#include "chrome/browser/sessions/session_restore.h"
#include "chrome/browser/sessions/session_service_log.h"
#include "chrome/browser/sessions/session_service_utils.h"
#include "chrome/browser/sessions/tab_restore_service_factory.h"
#include "chrome/browser/tab_group_sync/tab_group_sync_service_factory.h"
#include "chrome/browser/ui/browser_init_state.h"
#include "chrome/browser/ui/browser_tabstrip.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"
#include "chrome/browser/ui/session_crashed_bubble.h"
#include "chrome/browser/ui/startup/profile_launch_observer.h"
#include "chrome/browser/ui/startup/startup_browser_creator.h"
#include "chrome/browser/ui/startup/startup_tab.h"
#include "chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_utils.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/window_metadata/window_metadata_controller.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "chrome/common/chrome_switches.h"
#include "components/saved_tab_groups/public/tab_group_sync_service.h"
#include "components/sessions/content/content_serialized_navigation_builder.h"
#include "components/sessions/content/session_tab_helper.h"
#include "components/sessions/core/command_storage_manager.h"
#include "components/sessions/core/session_command.h"
#include "components/sessions/core/session_constants.h"
#include "components/sessions/core/session_types.h"
#include "components/sessions/core/tab_restore_service.h"
#include "content/public/browser/navigation_details.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/session_storage_namespace.h"
#include "content/public/browser/web_contents.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "chrome/browser/ash/crostini/crostini_util.h"
#include "chromeos/components/kiosk/kiosk_utils.h"
#endif  // BUILDFLAG(IS_CHROMEOS)

#if BUILDFLAG(IS_MAC)
#include "chrome/browser/app_controller_mac.h"
#endif

using content::NavigationEntry;
using content::WebContents;
using sessions::ContentSerializedNavigationBuilder;
using sessions::SerializedNavigationEntry;

namespace {

const void* const kInstanceTrackerKey = &kInstanceTrackerKey;

// An instance of this class is created pre-profile. It is used to track how
// many SessionServices have been created.
class InstanceTracker : public base::SupportsUserData::Data {
 public:
  InstanceTracker() = default;
  InstanceTracker(const InstanceTracker&) = delete;
  InstanceTracker& operator=(const InstanceTracker&) = delete;
  ~InstanceTracker() override = default;

  // Registers a new instance. Returns true is this is the first time called
  // with the specified profile.
  static bool RegisterNewSessionService(Profile* profile) {
    InstanceTracker* tracker = static_cast<InstanceTracker*>(
        profile->GetUserData(kInstanceTrackerKey));
    if (!tracker) {
      profile->SetUserData(kInstanceTrackerKey,
                           std::make_unique<InstanceTracker>());
      tracker = static_cast<InstanceTracker*>(
          profile->GetUserData(kInstanceTrackerKey));
    }
    return tracker->IncrementSessionServiceCount();
  }

 private:
  bool IncrementSessionServiceCount() {
    const bool is_first = session_service_count_ == 0;
    ++session_service_count_;
    return is_first;
  }

  int session_service_count_ = 0;
};

}  // namespace

SessionService::SessionService(Profile* profile)
    : SessionServiceBase(
          profile,
          SessionServiceBase::SessionServiceType::kSessionRestore),
      is_first_session_service_(
          InstanceTracker::RegisterNewSessionService(profile)) {
  if (is_first_session_service_) {
    LogSessionServiceStartEvent(profile, HasPendingUncleanExit(profile));
  }
  closing_all_browsers_subscription_ = chrome::AddClosingAllBrowsersCallback(
      base::BindRepeating(&SessionService::OnClosingAllBrowsersChanged,
                          base::Unretained(this)));
  ExitTypeService* exit_type_service =
      ExitTypeService::GetInstanceForProfile(profile);
  if (exit_type_service && exit_type_service->waiting_for_user_to_ack_crash()) {
    SetSavingEnabled(false);
    exit_type_service->AddCrashAckCallback(base::BindOnce(
        &SessionService::SetSavingEnabled, weak_factory_.GetWeakPtr(), true));
  }
}

SessionService::~SessionService() {
  base::UmaHistogramCounts100("SessionRestore.UnrecoverableWriteErrorCount",
                              unrecoverable_write_error_count_);

  // This must be called from SessionService because Save() calls back into
  // SessionService, which will have been destructed already if we try to
  // do this in SessionServiceBase.
  command_storage_manager()->Save();

  DestroyCommandStorageManager();

  // Certain code paths explicitly destroy the SessionService as part of
  // shutdown.
  if (!did_log_exit_) {
    LogExitEvent();
  }
}

// static
bool SessionService::IsRelevantWindowType(
    sessions::SessionWindow::WindowType window_type) {
  return (window_type == sessions::SessionWindow::TYPE_NORMAL) ||
         (window_type == sessions::SessionWindow::TYPE_POPUP);
}

bool SessionService::ShouldRestore(BrowserWindowInterface* browser) {
#if BUILDFLAG(IS_CHROMEOS)
  // Do not restore browser window in the kiosk session.
  if (chromeos::IsKioskSession()) {
    return false;
  }
#endif

  // ChromeOS and OSX have different ideas of application lifetime than
  // the other platforms.
  // On ChromeOS opening a new window should never start a new session.
#if BUILDFLAG(IS_CHROMEOS)
  // On Chrome OS, sessions are restored (or not) based on the startup setting.

  // If there are other browser windows, or during the restoring process, or
  // restore from crash, or should not restore for `browser`, sessions should
  // not be restored.
  if (SessionRestore::IsRestoring(profile()) || has_open_trackable_browsers_ ||
      HasPendingUncleanExit(profile()) ||
      (browser &&
       !BrowserInitState::From(browser)->should_trigger_session_restore())) {
    return false;
  }

  // If the on startup setting is not restore, sessions should not be
  // restored.
  SessionStartupPref pref =
      SessionStartupPref::GetStartupPref(profile()->GetPrefs());
  if (!pref.ShouldRestoreLastSession()) {
    return false;
  }

  if (!browser) {
    return true;
  }

  // App windows should not be restored.
  auto window_type = WindowTypeForBrowserType(browser->GetType());
  if (window_type == sessions::SessionWindow::TYPE_APP ||
      window_type == sessions::SessionWindow::TYPE_APP_POPUP) {
    return false;
  }

  // If the browser does not have a `restore_id`, then we restore the session.
  return BrowserInitState::From(browser)->create_params().restore_id ==
         BrowserWindowCreateParams::kDefaultRestoreId;
#else
  if (!has_open_trackable_browsers_ &&
      !StartupBrowserCreator::InSynchronousProfileLaunch() &&
      !SessionRestore::IsRestoring(profile())
#if BUILDFLAG(IS_MAC)
      // On OSX, a new window should not start a new session if it was opened
      // from the dock or the menubar.
      && !app_controller_mac::IsOpeningNewWindow()
#endif  // BUILDFLAG(IS_MAC)
  ) {
    return true;
  }
  return false;
#endif  // BUILDFLAG(IS_CHROMEOS)
}

bool SessionService::RestoreIfNecessary(const StartupTabs& startup_tabs,
                                        bool restore_apps) {
  return RestoreIfNecessary(startup_tabs, nullptr, restore_apps);
}

void SessionService::MoveCurrentSessionToLastSession() {
  DCHECK(is_saving_enabled());
  pending_tab_close_ids_.clear();
  window_closing_ids_.clear();
  pending_window_close_ids_.clear();

  command_storage_manager()->MoveCurrentSessionToLastSession();
  ScheduleResetCommands();
}

void SessionService::DeleteLastSession() {
  command_storage_manager()->DeleteLastSession();
  ++count_delete_last_session_for_testing_;
}

void SessionService::SetSplitTab(SessionID window_id,
                                 SessionID tab_id,
                                 std::optional<split_tabs::SplitTabId> split) {
  if (!ShouldTrackChangesToWindow(window_id)) {
    return;
  }

  // Tabs get unsplit as they close. However, if the whole window is closing
  // tabs should stay in their split. So, ignore this call in that case.
  if (pending_window_close_ids_.contains(window_id) ||
      window_closing_ids_.contains(window_id)) {
    return;
  }

  ScheduleCommand(sessions::CreateSplitTabCommand(tab_id, std::move(split)));
}

void SessionService::SetSplitTabData(
    SessionID window_id,
    const split_tabs::SplitTabId split,
    const split_tabs::SplitTabVisualData* visual_data) {
  if (!ShouldTrackChangesToWindow(window_id)) {
    return;
  }

  // Any split metadata changes happening in a closing window can be ignored.
  if (pending_window_close_ids_.contains(window_id) ||
      window_closing_ids_.contains(window_id)) {
    return;
  }

  ScheduleCommand(
      sessions::CreateSplitTabDataUpdateCommand(split, visual_data));
}

void SessionService::SetTabGroup(SessionID window_id,
                                 SessionID tab_id,
                                 std::optional<tab_groups::TabGroupId> group) {
  if (!ShouldTrackChangesToWindow(window_id)) {
    return;
  }

  // Tabs get ungrouped as they close. However, if the whole window is closing
  // tabs should stay in their groups. So, ignore this call in that case.
  if (pending_window_close_ids_.contains(window_id) ||
      window_closing_ids_.contains(window_id)) {
    return;
  }

  ScheduleCommand(sessions::CreateTabGroupCommand(tab_id, std::move(group)));
}

void SessionService::SetTabGroupMetadata(
    SessionID window_id,
    const tab_groups::TabGroupId& group_id,
    const tab_groups::TabGroupVisualData* visual_data) {
  tab_groups::TabGroupSyncService* tab_group_service =
      tab_groups::TabGroupSyncServiceFactory::GetForProfile(profile());
  std::optional<std::string> saved_guid;
  if (tab_group_service) {
    if (const std::optional<tab_groups::SavedTabGroup> saved_group =
            tab_group_service->GetGroup(group_id)) {
      saved_guid = saved_group->saved_guid().AsLowercaseString();
    } else if (auto it = local_to_sync_id_mapping_.find(group_id);
               it != local_to_sync_id_mapping_.end()) {
      saved_guid = it->second;
    }
  }

  SetTabGroupMetadata(window_id, group_id, visual_data, std::move(saved_guid));
}

void SessionService::SetTabGroupMetadata(
    SessionID window_id,
    const tab_groups::TabGroupId& group_id,
    const tab_groups::TabGroupVisualData* visual_data,
    std::optional<std::string> saved_guid) {
  if (!ShouldTrackChangesToWindow(window_id)) {
    return;
  }

  // Any group metadata changes happening in a closing window can be ignored.
  if (pending_window_close_ids_.contains(window_id) ||
      window_closing_ids_.contains(window_id)) {
    return;
  }

  ScheduleCommand(sessions::CreateTabGroupMetadataUpdateCommand(
      group_id, visual_data, std::move(saved_guid)));
}

void SessionService::AddSavedTabGroupsMapping(
    const tab_groups::TabGroupId& group_id,
    const std::string& saved_guid) {
  // TODO(crbug.com/376733439): Clear mapping when TabGroupSyncService has
  // finished initializing.
  CHECK(!local_to_sync_id_mapping_.contains(group_id));
  local_to_sync_id_mapping_.emplace(group_id, saved_guid);
}

void SessionService::AddTabExtraData(SessionID window_id,
                                     SessionID tab_id,
                                     const char* key,
                                     const std::string& data) {
  if (!ShouldTrackChangesToWindow(window_id)) {
    return;
  }

  ScheduleCommand(sessions::CreateAddTabExtraDataCommand(tab_id, key, data));
}

void SessionService::AddWindowExtraData(SessionID window_id,
                                        const char* key,
                                        const std::string& data) {
  if (!ShouldTrackChangesToWindow(window_id)) {
    return;
  }

  ScheduleCommand(
      sessions::CreateAddWindowExtraDataCommand(window_id, key, data));
}

void SessionService::TabClosed(SessionID window_id, SessionID tab_id) {
  if (!tab_id.id()) {
    return;  // Happens when the tab is replaced.
  }

  if (!ShouldTrackChangesToWindow(window_id)) {
    return;
  }

  auto i = tab_to_available_range()->find(tab_id);
  if (i != tab_to_available_range()->end()) {
    tab_to_available_range()->erase(i);
  }

  if (pending_window_close_ids_.contains(window_id)) {
    // Tab is in last window and the window is being closed. 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.
    // This is necessary to ensure the session is properly stored when closing
    // the final window.
    pending_tab_close_ids_.insert(tab_id);
  } else {
    // If an individual tab is being closed or a secondary window is being
    // closed, just mark the tab as closed now.
    ScheduleCommand(sessions::CreateTabClosedCommand(tab_id));
    if (!window_closing_ids_.contains(window_id) && IsOnlyOneTabLeft()) {
      // This is the last tab in the last tabbed browser.
      has_open_trackable_browsers_ = false;
    }
  }
}

void SessionService::WindowOpened(BrowserWindowInterface* browser) {
  if (!ShouldTrackBrowser(browser)) {
    return;
  }

  RestoreIfNecessary(StartupTabs(), browser, /* restore_apps */ false);
  SetWindowType(browser->GetSessionID(), browser->GetType());
  SetWindowAppName(browser->GetSessionID(),
                   BrowserInitState::From(browser)->create_params().app_name);
  SetWindowUserTitle(browser->GetSessionID(),
                     WindowMetadataController::From(browser)->user_title());

  // Save a browser workspace after window is created in `Browser()`.
  // Bento desks restore feature in ash requires this line to restore correctly
  // after creating a new browser window in a particular desk.
  SetWindowWorkspace(browser->GetSessionID(),
                     BrowserWindow::FromBrowser(browser)->GetWorkspace());
  SetWindowVisibleOnAllWorkspaces(
      browser->GetSessionID(),
      BrowserWindow::FromBrowser(browser)->IsVisibleOnAllWorkspaces());
}

void SessionService::WindowClosing(SessionID window_id) {
  if (!ShouldTrackChangesToWindow(window_id)) {
    return;
  }

  // If Chrome is closed immediately after a history deletion, we have to
  // rebuild commands before this window is closed, otherwise these tabs would
  // be lost.
  RebuildCommandsIfRequired();

  // 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);
  }
  bool use_pending_close = !has_open_trackable_browsers_;
  if (!use_pending_close) {
    // Somewhat outside of "normal behavior" is profile locking.  In this case
    // (when IsSiginRequired has already been set True), we're closing all
    // browser windows in turn but want them all to be restored when the user
    // unlocks.  To accomplish this, we do a "pending close" on all windows
    // instead of just the last one (which has no open_trackable_browsers).
    // http://crbug.com/41097527
    //
    // Some editions (like iOS) don't have a profile_manager and some tests
    // don't supply one so be lenient.
    if (g_browser_process) {
      ProfileManager* profile_manager = g_browser_process->profile_manager();
      if (profile_manager) {
        ProfileAttributesEntry* entry =
            profile_manager->GetProfileAttributesStorage()
                .GetProfileAttributesWithPath(profile()->GetPath());
        use_pending_close = entry && entry->IsSigninRequired();
      }
    }
  }
  if (use_pending_close) {
    LogExitEvent();
    pending_window_close_ids_.insert(window_id);
  } else {
    window_closing_ids_.insert(window_id);
  }
}

void SessionService::WindowClosed(SessionID window_id) {
  windows_tracking()->erase(window_id);
  last_selected_tab_in_window()->erase(window_id);

  if (window_closing_ids_.contains(window_id)) {
    window_closing_ids_.erase(window_id);
    ScheduleCommand(sessions::CreateWindowClosedCommand(window_id));
  } else if (!pending_window_close_ids_.contains(window_id)) {
    // We'll hit this if user closed the last tab in a window.
    has_open_trackable_browsers_ = HasOpenTrackableBrowsers(window_id);
    if (!has_open_trackable_browsers_) {
      LogExitEvent();
      pending_window_close_ids_.insert(window_id);
    } else {
      ScheduleCommand(sessions::CreateWindowClosedCommand(window_id));
    }
  }
}

void SessionService::SetWindowType(SessionID window_id,
                                   BrowserWindowInterface::Type type) {
  sessions::SessionWindow::WindowType window_type =
      WindowTypeForBrowserType(type);
  if (!ShouldRestoreWindowOfType(window_type)) {
    return;
  }

  windows_tracking()->insert(window_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, window_type));
}

void SessionService::SetWindowUserTitle(SessionID window_id,
                                        const std::string& user_title) {
  if (!ShouldTrackChangesToWindow(window_id)) {
    return;
  }

  ScheduleCommand(
      sessions::CreateSetWindowUserTitleCommand(window_id, user_title));
}

void SessionService::OnErrorWritingSessionCommands() {
  // TODO(sky): if `pending_window_close_ids_` is non-empty, then
  // RebuildCommandsIfRequired() will not call ScheduleResetCommands(). This is
  // because a rebuild can't happen (because the browsers in
  // `pending_window_close_ids_` have been deleted). My hope is that this
  // happens seldom enough that we don't need to deal with this case, as it will
  // necessitate some amount of snapshotting in memory when a window is closing.
  // The histogram should give us an idea of how often this happens in practice.
  const bool unrecoverable_write_error = !pending_window_close_ids_.empty();
  if (unrecoverable_write_error) {
    ++unrecoverable_write_error_count_;
  }
  LogSessionServiceWriteErrorEvent(profile(), unrecoverable_write_error);
  set_rebuild_on_next_save(true);
  RebuildCommandsIfRequired();
}

void SessionService::SetTabUserAgentOverride(
    SessionID window_id,
    SessionID tab_id,
    const sessions::SerializedUserAgentOverride& user_agent_override) {
  if (!ShouldTrackChangesToWindow(window_id)) {
    return;
  }

  ScheduleCommand(sessions::CreateSetTabUserAgentOverrideCommand(
      tab_id, user_agent_override));
}

BrowserWindowInterface::Type
SessionService::GetDesiredBrowserTypeForWebContents() {
  return BrowserWindowInterface::Type::TYPE_NORMAL;
}

void SessionService::DidScheduleCommand() {
  if (did_schedule_command_) {
    return;
  }
  did_schedule_command_ = true;
  if (is_first_session_service_) {
    return;
  }

  // TODO(crbug.com/40196304): A command has been scheduled for a SessionService
  // other than the first. Recreating the SessionService happens if shutdown is
  // canceled, which is valid, but bugs seem to indicate we are getting here in
  // scenarios we don't expect. Investigate further.
}

bool SessionService::ShouldRestoreWindowOfType(
    sessions::SessionWindow::WindowType window_type) const {
  // TYPE_APP and TYPE_APP_POPUP are handled by app_session_service.
  return IsRelevantWindowType(window_type);
}

bool SessionService::RestoreIfNecessary(const StartupTabs& startup_tabs,
                                        BrowserWindowInterface* browser,
                                        bool restore_apps) {
  if (ShouldRestore(browser)) {
    // 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_ && is_saving_enabled()) {
      // Make the current session the last.
      MoveCurrentSessionToLastSession();
      move_on_new_browser_ = false;
    }
    SessionStartupPref pref = StartupBrowserCreator::GetSessionStartupPref(
        *base::CommandLine::ForCurrentProcess(), profile());
    sessions::TabRestoreService* tab_restore_service =
        TabRestoreServiceFactory::GetForProfileIfExisting(profile());
    if (pref.ShouldRestoreLastSession() &&
        (!tab_restore_service || !tab_restore_service->IsRestoring())) {
      SessionRestore::RestoreSession(
          profile(), browser,
          SessionRestore::RESTORE_BROWSER |
              (browser ? 0 : SessionRestore::ALWAYS_CREATE_TABBED_BROWSER) |
              (restore_apps ? SessionRestore::RESTORE_APPS : 0),
          startup_tabs);
      return true;
    }
#if BUILDFLAG(IS_CHROMEOS)
  } else if (HasPendingUncleanExit(profile())) {
    if (!browser) {
      // If 'browser' is null, call StartupBrowserCreator to create a new
      // browser instance.
      base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
      StartupBrowserCreator browser_creator;
      browser_creator.LaunchBrowser(*command_line, profile(), base::FilePath(),
                                    chrome::startup::IsProcessStartup::kYes,
                                    chrome::startup::IsFirstRun::kNo,
                                    /*restore_tabbed_browser=*/true);
      return true;
    } else {
      // If 'browser' is not null, show the crash bubble in the current browser
      // instance.
      SessionCrashedBubble::ShowIfNotOffTheRecordProfile(
          browser, /*skip_tab_checking=*/true);
      ProfileLaunchObserver::AddLaunched(profile());
    }
#endif  // BUILDFLAG(IS_CHROMEOS)
  }
  return false;
}

void SessionService::BuildCommandsForTab(
    SessionID window_id,
    WebContents* tab,
    int index_in_window,
    std::optional<tab_groups::TabGroupId> group,
    std::optional<split_tabs::SplitTabId> split,
    bool is_pinned,
    IdToRange* tab_to_available_range) {
  DCHECK(is_saving_enabled());
  SessionServiceBase::BuildCommandsForTab(window_id, tab, index_in_window,
                                          group, split, is_pinned,
                                          tab_to_available_range);

  sessions::SessionTabHelper* session_tab_helper =
      sessions::SessionTabHelper::FromWebContents(tab);
  SessionID session_id(session_tab_helper->session_id());

  const blink::UserAgentOverride& ua_override = tab->GetUserAgentOverride();

  if (!ua_override.ua_string_override.empty()) {
    sessions::SerializedUserAgentOverride serialized_ua_override;
    serialized_ua_override.ua_string_override = ua_override.ua_string_override;
    serialized_ua_override.opaque_ua_metadata_override =
        blink::UserAgentMetadata::Marshal(ua_override.ua_metadata_override);

    command_storage_manager()->AppendRebuildCommand(
        sessions::CreateSetTabUserAgentOverrideCommand(session_id,
                                                       serialized_ua_override));
  }

  if (group.has_value()) {
    command_storage_manager()->AppendRebuildCommand(
        sessions::CreateTabGroupCommand(session_id, std::move(group)));
  }

  if (split.has_value()) {
    command_storage_manager()->AppendRebuildCommand(
        sessions::CreateSplitTabCommand(session_id, std::move(split)));
  }
}

void SessionService::ScheduleResetCommands() {
  DCHECK(is_saving_enabled());
  command_storage_manager()->set_pending_reset(true);
  command_storage_manager()->ClearPendingCommands();
  tab_to_available_range()->clear();
  windows_tracking()->clear();
  last_selected_tab_in_window()->clear();
  set_rebuild_on_next_save(false);
  BuildCommandsFromBrowsers(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;
  }
  command_storage_manager()->StartSaveTimer();
}

void SessionService::CommitPendingCloses() {
  for (auto id : pending_tab_close_ids_) {
    ScheduleCommand(sessions::CreateTabClosedCommand(id));
  }
  pending_tab_close_ids_.clear();

  for (auto id : pending_window_close_ids_) {
    ScheduleCommand(sessions::CreateWindowClosedCommand(id));
  }
  pending_window_close_ids_.clear();

  RemoveExitEvent();
}

bool SessionService::IsOnlyOneTabLeft() const {
  if (profile()->AsTestingProfile()) {
    return is_only_one_tab_left_for_test_;
  }

  int window_count = 0;
  bool is_only_one_tab_left = true;
  ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
      [this, &is_only_one_tab_left,
       &window_count](BrowserWindowInterface* browser) {
        const SessionID window_id = browser->GetSessionID();
        if (ShouldTrackBrowser(browser) &&
            !window_closing_ids_.contains(window_id)) {
          if (++window_count > 1) {
            is_only_one_tab_left = false;
          }
          // By the time this is invoked the tab has been removed. As such, we
          // use > 0 here rather than > 1.
          if (browser->GetTabStripModel()->count() > 0) {
            is_only_one_tab_left = false;
          }
        }
        // Continue until the above checks confirm there is more than one
        // trackable tab left, or all browsers are exhausted.
        return is_only_one_tab_left;
      });
  return is_only_one_tab_left;
}

bool SessionService::HasOpenTrackableBrowsers(SessionID window_id) const {
  if (profile()->AsTestingProfile()) {
    return has_open_trackable_browser_for_test_;
  }

  bool has_open_trackable = false;
  ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
      [this, window_id, &has_open_trackable](BrowserWindowInterface* browser) {
        const SessionID browser_id = browser->GetSessionID();
        if (browser_id != window_id &&
            !window_closing_ids_.contains(browser_id)) {
          if (ShouldTrackBrowser(browser)) {
            has_open_trackable = true;
          }
        }
        return !has_open_trackable;
      });
  return has_open_trackable;
}

void SessionService::RebuildCommandsIfRequired() {
  if (rebuild_on_next_save() && pending_window_close_ids_.empty() &&
      is_saving_enabled()) {
    ScheduleResetCommands();
  }
}

void SessionService::OnClosingAllBrowsersChanged(bool closing) {
  if (closing) {
    LogExitEvent();
  }
}

void SessionService::LogExitEvent() {
  // If there are pending closes, then we have already logged the exit.
  if (!pending_window_close_ids_.empty()) {
    return;
  }

  RemoveExitEvent();
  int browser_count = 0;
  int tab_count = 0;
  ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
      [this, &browser_count, &tab_count](BrowserWindowInterface* browser) {
        if (browser->GetProfile() == profile()) {
          ++browser_count;
          tab_count += browser->GetTabStripModel()->count();
        }
        return true;
      });
  did_log_exit_ = true;
  LogSessionServiceExitEvent(profile(), browser_count, tab_count,
                             is_first_session_service_, did_schedule_command_);
}

void SessionService::RemoveExitEvent() {
  if (!did_log_exit_) {
    return;
  }

  RemoveLastSessionServiceEventOfType(profile(),
                                      SessionServiceEventLogType::kExit);
  did_log_exit_ = false;
}
