chromium/ios/chrome/browser/sessions/model/session_restoration_service_factory.mm

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/sessions/model/session_restoration_service_factory.h"

#import "base/files/file_util.h"
#import "base/functional/callback.h"
#import "base/metrics/histogram_functions.h"
#import "base/notreached.h"
#import "base/task/task_traits.h"
#import "base/task/thread_pool.h"
#import "base/types/cxx23_to_underlying.h"
#import "components/keyed_service/ios/browser_state_dependency_manager.h"
#import "components/pref_registry/pref_registry_syncable.h"
#import "components/prefs/pref_service.h"
#import "ios/chrome/browser/sessions/model/features.h"
#import "ios/chrome/browser/sessions/model/legacy_session_restoration_service.h"
#import "ios/chrome/browser/sessions/model/session_constants.h"
#import "ios/chrome/browser/sessions/model/session_migration.h"
#import "ios/chrome/browser/sessions/model/session_restoration_service_impl.h"
#import "ios/chrome/browser/sessions/model/session_service_ios.h"
#import "ios/chrome/browser/sessions/model/web_session_state_cache_factory.h"
#import "ios/chrome/browser/shared/model/browser_state/browser_state_otr_helper.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/web/public/web_state_id.h"

namespace {

// Alias for readability.
using RequestedStorageFormat = SessionRestorationServiceFactory::StorageFormat;

// Threshold before retrying to migration the session storage.
constexpr base::TimeDelta kRetryMigrationThreshold = base::Days(3);

// Value taken from Desktop Chrome.
constexpr base::TimeDelta kSaveDelay = base::Seconds(2.5);

// Returns the value of the session storage format pref.
SessionStorageFormat GetSessionStorageFormatPref(PrefService* prefs) {
  switch (prefs->GetInteger(kSessionStorageFormatPref)) {
    case base::to_underlying(SessionStorageFormat::kLegacy):
      return SessionStorageFormat::kLegacy;

    case base::to_underlying(SessionStorageFormat::kOptimized):
      return SessionStorageFormat::kOptimized;

    default:
      return SessionStorageFormat::kUnknown;
  }
}

// Returns the value of the session storage migration status pref.
SessionStorageMigrationStatus GetSessionStorageMigrationStatusPref(
    PrefService* prefs) {
  switch (prefs->GetInteger(kSessionStorageMigrationStatusPref)) {
    case base::to_underlying(SessionStorageMigrationStatus::kSuccess):
      return SessionStorageMigrationStatus::kSuccess;

    case base::to_underlying(SessionStorageMigrationStatus::kFailure):
      return SessionStorageMigrationStatus::kFailure;

    case base::to_underlying(SessionStorageMigrationStatus::kInProgress):
      return SessionStorageMigrationStatus::kInProgress;

    default:
      return SessionStorageMigrationStatus::kUnkown;
  }
}

// Returns whether `format` corresponds to `requested_format`.
bool IsSessionStorageInRequestedFormat(
    SessionStorageFormat format,
    RequestedStorageFormat requested_format) {
  switch (requested_format) {
    case RequestedStorageFormat::kLegacy:
      return format == SessionStorageFormat::kLegacy;

    case RequestedStorageFormat::kOptimized:
      return format == SessionStorageFormat::kOptimized;
  }
}

// Converts `requested_format` to SessionStorageFormat according to the
// status of the migration operation.
SessionStorageFormat SessionStorageFormatFromRequestedFormat(
    RequestedStorageFormat requested_format,
    ios::sessions::MigrationResult::Status status) {
  switch (status) {
    case ios::sessions::MigrationResult::Status::kSuccess:
      return requested_format == RequestedStorageFormat::kLegacy
                 ? SessionStorageFormat::kLegacy
                 : SessionStorageFormat::kOptimized;

    case ios::sessions::MigrationResult::Status::kFailure:
      return requested_format != RequestedStorageFormat::kLegacy
                 ? SessionStorageFormat::kLegacy
                 : SessionStorageFormat::kOptimized;
  }
}

// Store the session storage format `storage_format` metric.
void RecordSessionStorageFormatMetric(SessionStorageFormat storage_format) {
  base::UmaHistogramEnumeration(
      kSessionHistogramStorageFormat,
      storage_format == SessionStorageFormat::kLegacy
          ? SessionHistogramStorageFormat::kLegacy
          : SessionHistogramStorageFormat::kOptimized);
}

// Store the session storage format `storage_format` and the migration
// status `migration_status` metrics.
void RecordSessionStorageFormatAndMigrationStatusMetrics(
    SessionStorageFormat storage_format,
    SessionStorageMigrationStatus migration_status) {
  RecordSessionStorageFormatMetric(storage_format);

  SessionHistogramStorageMigrationStatus histogram_status;
  switch (migration_status) {
    case SessionStorageMigrationStatus::kSuccess:
      histogram_status = SessionHistogramStorageMigrationStatus::kSuccess;
      break;

    case SessionStorageMigrationStatus::kFailure:
      histogram_status = SessionHistogramStorageMigrationStatus::kFailure;
      break;

    case SessionStorageMigrationStatus::kInProgress:
      histogram_status = SessionHistogramStorageMigrationStatus::kInterrupted;
      break;

    case SessionStorageMigrationStatus::kUnkown:
      NOTREACHED();
  }

  base::UmaHistogramEnumeration(kSessionHistogramStorageMigrationStatus,
                                histogram_status);
}

// Store the session storage format `storage_format` and the migration
// status `migration_status` to `pref_service`.
void RecordSessionStorageFormatAndMigrationStatus(
    PrefService* pref_service,
    SessionStorageFormat storage_format,
    SessionStorageMigrationStatus migration_status) {
  pref_service->SetInteger(kSessionStorageFormatPref,
                           base::to_underlying(storage_format));

  pref_service->SetInteger(kSessionStorageMigrationStatusPref,
                           base::to_underlying(migration_status));
}

// Detects the storage format for session at `path`. If no existing storage
// is found, return that the storage corresponds to `requested_format`.
SessionStorageFormat DetectStorageFormat(
    const base::FilePath& path,
    RequestedStorageFormat requested_format) {
  if (base::DirectoryExists(path.Append(kSessionRestorationDirname))) {
    return SessionStorageFormat::kOptimized;
  }

  if (base::DirectoryExists(path.Append(kLegacySessionsDirname))) {
    return SessionStorageFormat::kLegacy;
  }

  return SessionStorageFormatFromRequestedFormat(
      requested_format, ios::sessions::MigrationResult::Status::kSuccess);
}

// Invoked when the session storage format has been detected.
void OnStorageFormatDetected(
    base::WeakPtr<ChromeBrowserState> weak_browser_state,
    base::OnceClosure closure,
    SessionStorageFormat storage_format) {
  ChromeBrowserState* browser_state = weak_browser_state.get();
  if (!browser_state) {
    return;
  }

  RecordSessionStorageFormatAndMigrationStatus(
      browser_state->GetPrefs(), storage_format,
      SessionStorageMigrationStatus::kSuccess);

  RecordSessionStorageFormatMetric(storage_format);

  std::move(closure).Run();
}

// Invoked when the session migration completes.
void OnSessionMigrationDone(
    base::WeakPtr<ChromeBrowserState> weak_browser_state,
    RequestedStorageFormat requested_format,
    base::TimeTicks migration_start,
    int32_t next_session_identifier,
    base::OnceClosure closure,
    ios::sessions::MigrationResult result) {
  ChromeBrowserState* browser_state = weak_browser_state.get();
  if (!browser_state) {
    return;
  }

  const SessionStorageMigrationStatus migration_status =
      result.status == ios::sessions::MigrationResult::Status::kSuccess
          ? SessionStorageMigrationStatus::kSuccess
          : SessionStorageMigrationStatus::kFailure;

  if (result.status == ios::sessions::MigrationResult::Status::kSuccess) {
    DCHECK_GE(result.next_session_identifier, next_session_identifier);
    if (result.next_session_identifier != next_session_identifier) {
      const int count =
          result.next_session_identifier - next_session_identifier;
      for (int i = 0; i < count; ++i) {
        std::ignore = web::WebStateID::NewUnique();
      }
    }
  }

  const SessionStorageFormat storage_format =
      SessionStorageFormatFromRequestedFormat(requested_format, result.status);

  RecordSessionStorageFormatAndMigrationStatus(
      browser_state->GetPrefs(), storage_format, migration_status);

  RecordSessionStorageFormatAndMigrationStatusMetrics(storage_format,
                                                      migration_status);

  base::UmaHistogramTimes(kSessionHistogramStorageMigrationTiming,
                          base::TimeTicks::Now() - migration_start);

  std::move(closure).Run();
}

}  // namespace

// static
SessionRestorationService* SessionRestorationServiceFactory::GetForBrowserState(
    ChromeBrowserState* browser_state) {
  return static_cast<SessionRestorationService*>(
      GetInstance()->GetServiceForBrowserState(browser_state, true));
}

// static
SessionRestorationServiceFactory*
SessionRestorationServiceFactory::GetInstance() {
  static base::NoDestructor<SessionRestorationServiceFactory> instance;
  return instance.get();
}

SessionRestorationServiceFactory::SessionRestorationServiceFactory()
    : BrowserStateKeyedServiceFactory(
          "SessionRestorationService",
          BrowserStateDependencyManager::GetInstance()) {
  DependsOn(WebSessionStateCacheFactory::GetInstance());
}

void SessionRestorationServiceFactory::MigrateSessionStorageFormat(
    ChromeBrowserState* browser_state,
    StorageFormat requested_format,
    base::OnceClosure closure) {
  DCHECK(!browser_state->IsOffTheRecord());
  DCHECK(!GetServiceForBrowserState(browser_state, false));

  PrefService* const prefs = browser_state->GetPrefs();
  const SessionStorageMigrationStatus status =
      GetSessionStorageMigrationStatusPref(prefs);

  // Nothing to do, the storage is already in the requested format.
  const SessionStorageFormat format = GetSessionStorageFormatPref(prefs);
  if (IsSessionStorageInRequestedFormat(format, requested_format)) {
    // It is possible for status to not be "success" if the flag controlling
    // `requested_format` changed between invocation. In that case migration
    // would have been attempted in the previous run and failed. If this is
    // the case, then reset the status to "success" to allow attempting the
    // migration if the flag is flipped again in the future.
    if (status != SessionStorageMigrationStatus::kSuccess) {
      RecordSessionStorageFormatAndMigrationStatus(
          prefs, format, SessionStorageMigrationStatus::kSuccess);
    }

    RecordSessionStorageFormatAndMigrationStatusMetrics(
        format, SessionStorageMigrationStatus::kSuccess);

    return std::move(closure).Run();
  }

  // If the format is unknown, do not try to migrate, instead detect which
  // format is used and stay on the existing format. The migration will be
  // attempted on next application restart if necessary.
  if (format == SessionStorageFormat::kUnknown) {
    base::ThreadPool::PostTaskAndReplyWithResult(
        FROM_HERE, {base::TaskPriority::USER_BLOCKING, base::MayBlock{}},
        base::BindOnce(&DetectStorageFormat, browser_state->GetStatePath(),
                       requested_format),
        base::BindOnce(&OnStorageFormatDetected, browser_state->AsWeakPtr(),
                       std::move(closure)));
    return;
  }

  // The status should only be "unknown" on the first run, or when upgrading
  // from M-121 or earlier. In both case, the format would also be "unknown"
  // and the storage format detection logic called which will set the status
  // to "success". This means that neither the format nor the status can be
  // "unknown" when reaching this point.
  DCHECK_NE(format, SessionStorageFormat::kUnknown);
  DCHECK_NE(status, SessionStorageMigrationStatus::kUnkown);

  if (status == SessionStorageMigrationStatus::kFailure ||
      status == SessionStorageMigrationStatus::kInProgress) {
    // The previous attempt either failed, or was interrupted (either by the
    // user or due to a crash). Retry the migration if enough time has passed
    // since the last attempt (hopefully the reason that caused it to fail or
    // to be interrupted has changed), otherwise skip the migration and log
    // the failure.
    const base::Time last_attempt_time =
        prefs->GetTime(kSessionStorageMigrationStartedTimePref);

    if (base::Time::Now() - last_attempt_time < kRetryMigrationThreshold) {
      RecordSessionStorageFormatAndMigrationStatusMetrics(format, status);
      return std::move(closure).Run();
    }
  }

  // The migration is required. Update the migration status to "in progress"
  // and start the asynchronous migration on a background sequence. Record
  // the time of the migration start in order to periodically retry it in
  // case of failure.
  prefs->SetInteger(
      kSessionStorageMigrationStatusPref,
      base::to_underlying(SessionStorageMigrationStatus::kInProgress));
  prefs->SetTime(kSessionStorageMigrationStartedTimePref, base::Time::Now());

  // Migrate all session in `browser_state`'s and OTR state paths.
  std::vector<base::FilePath> paths = {
      browser_state->GetStatePath(),
      browser_state->GetOffTheRecordStatePath(),
  };

  const web::WebStateID identifier = web::WebStateID::NewUnique();
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::TaskPriority::USER_BLOCKING, base::MayBlock{}},
      base::BindOnce(requested_format == StorageFormat::kLegacy
                         ? &ios::sessions::MigrateSessionsInPathsToLegacy
                         : &ios::sessions::MigrateSessionsInPathsToOptimized,
                     std::move(paths), identifier.identifier()),
      base::BindOnce(&OnSessionMigrationDone, browser_state->AsWeakPtr(),
                     requested_format, base::TimeTicks::Now(),
                     identifier.identifier(), std::move(closure)));
}

SessionRestorationServiceFactory::~SessionRestorationServiceFactory() = default;

std::unique_ptr<KeyedService>
SessionRestorationServiceFactory::BuildServiceInstanceFor(
    web::BrowserState* context) const {
  ChromeBrowserState* browser_state =
      ChromeBrowserState::FromBrowserState(context);

  scoped_refptr<base::SequencedTaskRunner> task_runner =
      base::ThreadPool::CreateSingleThreadTaskRunner(
          {base::MayBlock(), base::TaskPriority::USER_VISIBLE,
           base::TaskShutdownBehavior::BLOCK_SHUTDOWN},
          base::SingleThreadTaskRunnerThreadMode::DEDICATED);

  const base::FilePath storage_path = browser_state->GetStatePath();

  SessionStorageFormat format =
      GetSessionStorageFormatPref(browser_state->GetPrefs());

  // During unit tests, it is the method MigrateSessionStorageFormat(...)
  // will not be called before the service is created and the preference
  // will have its default value of `SessionStorageFormat::kUnknown`. Use
  // the feature flag to select which implementation to use.
  if (format == SessionStorageFormat::kUnknown) {
    format = session::features::UseSessionSerializationOptimizations()
                 ? SessionStorageFormat::kOptimized
                 : SessionStorageFormat::kLegacy;
  }

  // If the optimised session restoration format is not enabled, create a
  // LegacySessionRestorationService instance which wraps the legacy API.
  if (format == SessionStorageFormat::kLegacy) {
    SessionServiceIOS* session_service_ios =
        [[SessionServiceIOS alloc] initWithSaveDelay:kSaveDelay
                                          taskRunner:task_runner];

    return std::make_unique<LegacySessionRestorationService>(
        IsPinnedTabsEnabled(), IsTabGroupInGridEnabled(), storage_path,
        session_service_ios,
        WebSessionStateCacheFactory::GetForBrowserState(browser_state));
  }

  return std::make_unique<SessionRestorationServiceImpl>(
      kSaveDelay, IsPinnedTabsEnabled(), IsTabGroupInGridEnabled(),
      storage_path, task_runner);
}

web::BrowserState* SessionRestorationServiceFactory::GetBrowserStateToUse(
    web::BrowserState* context) const {
  return GetBrowserStateOwnInstanceInIncognito(context);
}

void SessionRestorationServiceFactory::RegisterBrowserStatePrefs(
    user_prefs::PrefRegistrySyncable* registry) {
  registry->RegisterIntegerPref(
      kSessionStorageFormatPref,
      base::to_underlying(SessionStorageFormat::kUnknown));
  registry->RegisterIntegerPref(
      kSessionStorageMigrationStatusPref,
      base::to_underlying(SessionStorageMigrationStatus::kUnkown));
  registry->RegisterTimePref(kSessionStorageMigrationStartedTimePref,
                             base::Time());
}