chromium/components/previous_session_info/previous_session_info.mm

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

#import "components/previous_session_info/previous_session_info.h"

#include <mach/mach.h>

#import <UIKit/UIKit.h>

#import "base/ios/ios_util.h"
#import "base/metrics/field_trial.h"
#import "base/strings/sys_string_conversions.h"
#import "base/system/sys_info.h"
#import "base/time/time.h"
#import "base/timer/timer.h"
#import "components/previous_session_info/previous_session_info_private.h"
#import "components/variations/variations_crash_keys.h"
#import "components/version_info/version_info.h"

using previous_session_info_constants::DeviceBatteryState;
using previous_session_info_constants::DeviceThermalState;
using previous_session_info_constants::kPreviousSessionInfoParamsPrefix;

namespace {

// Returns timestamp (in seconds since January 2001) when OS has started.
NSTimeInterval GetOSStartTimeIntervalSinceReferenceDate() {
  return NSDate.timeIntervalSinceReferenceDate -
         NSProcessInfo.processInfo.systemUptime;
}

// Translates a UIDeviceBatteryState value to DeviceBatteryState value.
DeviceBatteryState GetBatteryStateFromUIDeviceBatteryState(
    UIDeviceBatteryState device_battery_state) {
  switch (device_battery_state) {
    case UIDeviceBatteryStateUnknown:
      return DeviceBatteryState::kUnknown;
    case UIDeviceBatteryStateUnplugged:
      return DeviceBatteryState::kUnplugged;
    case UIDeviceBatteryStateCharging:
      return DeviceBatteryState::kCharging;
    case UIDeviceBatteryStateFull:
      return DeviceBatteryState::kFull;
  }

  return DeviceBatteryState::kUnknown;
}

// Translates a NSProcessInfoThermalState value to DeviceThermalState value.
DeviceThermalState GetThermalStateFromNSProcessInfoThermalState(
    NSProcessInfoThermalState process_info_thermal_state) {
  switch (process_info_thermal_state) {
    case NSProcessInfoThermalStateNominal:
      return DeviceThermalState::kNominal;
    case NSProcessInfoThermalStateFair:
      return DeviceThermalState::kFair;
    case NSProcessInfoThermalStateSerious:
      return DeviceThermalState::kSerious;
    case NSProcessInfoThermalStateCritical:
      return DeviceThermalState::kCritical;
  }

  return DeviceThermalState::kUnknown;
}

// NSUserDefaults keys.
// - The (string) application version.
NSString* const kLastRanVersion = @"LastRanVersion";
// - The (string) device language.
NSString* const kLastRanLanguage = @"LastRanLanguage";
// - The (integer) available device storage, in kilobytes.
NSString* const kPreviousSessionInfoAvailableDeviceStorage =
    @"PreviousSessionInfoAvailableDeviceStorage";
// - The (float) battery charge level.
NSString* const kPreviousSessionInfoBatteryLevel =
    @"PreviousSessionInfoBatteryLevel";
// - The (integer) underlying value of the DeviceBatteryState enum representing
//   the device battery state.
NSString* const kPreviousSessionInfoBatteryState =
    @"PreviousSessionInfoBatteryState";
// - The (Date) of the recording start.
NSString* const kPreviousSessionInfoStartTime = @"PreviousSessionInfoStartTime";
// - The (Date) of the estimated end of the session.
NSString* const kPreviousSessionInfoEndTime = @"PreviousSessionInfoEndTime";
// - The (string) OS version.
NSString* const kPreviousSessionInfoOSVersion = @"PreviousSessionInfoOSVersion";
// - The (integer) underlying value of the DeviceThermalState enum representing
//   the device thermal state.
NSString* const kPreviousSessionInfoThermalState =
    @"PreviousSessionInfoThermalState";
// - A (boolean) describing whether the last session received
// ApplicationWillTerminate Notification.
NSString* const kPreviousSessionInfoAppWillTerminate =
    @"PreviousSessionInfoAppWillTerminate";

// Return a key prefixed with the params prefix.
NSString* ReportParamKey(NSString* key) {
  return [NSString
      stringWithFormat:@"%@%@", kPreviousSessionInfoParamsPrefix, key];
}

// Objective-C bridge to observe changes in the FieldTrialList.
class FieldTrialListObserverBridge : public base::FieldTrialList::Observer {
 public:
  explicit FieldTrialListObserverBridge() {}

 private:
  FieldTrialListObserverBridge(const PreviousSessionInfo&) = delete;
  FieldTrialListObserverBridge& operator=(const FieldTrialListObserverBridge&) =
      delete;

  // base::FieldTrialList::Observer:
  void OnFieldTrialGroupFinalized(const base::FieldTrial& trial,
                                  const std::string& group_name) override {
    dispatch_async(dispatch_get_main_queue(), ^{
      variations::ExperimentListInfo info = variations::GetExperimentListInfo();

      // Normally this call would go through -setReportParameterValue, which
      // calls -setObject and -synchronize on NSUserDefaults. However, since
      // this call is really noisy, just call setObject and skip synchronize.
      [NSUserDefaults.standardUserDefaults
          setObject:base::SysUTF8ToNSString(
                        base::NumberToString(info.num_experiments))
             forKey:ReportParamKey(@"num-experiments")];
      [NSUserDefaults.standardUserDefaults
          setObject:base::SysUTF8ToNSString(info.experiment_list)
             forKey:ReportParamKey(@"variations")];
    });
  }
};

}  // namespace

namespace previous_session_info_constants {
NSString* const kPreviousSessionInfoApplicationState =
    @"PreviousSessionInfoApplicationState";
NSString* const kDidSeeMemoryWarningShortlyBeforeTerminating =
    @"DidSeeMemoryWarning";
NSString* const kOSStartTime = @"OSStartTime";
NSString* const kPreviousSessionInfoRestoringSession =
    @"PreviousSessionInfoRestoringSession";
NSString* const kPreviousSessionInfoConnectedSceneSessionIDs =
    @"PreviousSessionInfoConnectedSceneSessionIDs";
NSString* const kPreviousSessionInfoParamsPrefix =
    @"PreviousSessionInfoParams.";
NSString* const kPreviousSessionInfoMemoryFootprint =
    @"PreviousSessionInfoMemoryFootprint";
NSString* const kPreviousSessionInfoTabCount = @"PreviousSessionInfoTabCount";
NSString* const kPreviousSessionInfoInactiveTabCount =
    @"PreviousSessionInfoInactiveTabCount";
NSString* const kPreviousSessionInfoOTRTabCount =
    @"PreviousSessionInfoOTRTabCount";
NSString* const kPreviousSessionInfoWarmStartCount =
    @"PreviousSessionInfoWarmStartCount";
}  // namespace previous_session_info_constants

@interface PreviousSessionInfo () {
  // Observe updates to field trial list.
  std::unique_ptr<FieldTrialListObserverBridge> _fieldTrialListObserver;
}

// Whether beginRecordingCurrentSession was called.
@property(nonatomic, assign) BOOL didBeginRecordingCurrentSession;

// Whether recording data is in progress.
@property(nonatomic, assign) BOOL recordingCurrentSession;

// Used for setting and resetting kPreviousSessionInfoRestoringSession flag.
// Can be greater than one if multiple sessions are being restored in parallel.
@property(atomic, assign) int numberOfSessionsBeingRestored;

// Redefined to be read-write.
@property(nonatomic, assign) NSInteger availableDeviceStorage;
@property(nonatomic, assign) float deviceBatteryLevel;
@property(nonatomic, assign) DeviceBatteryState deviceBatteryState;
@property(nonatomic, assign) DeviceThermalState deviceThermalState;
@property(nonatomic, assign) BOOL didSeeMemoryWarningShortlyBeforeTerminating;
@property(nonatomic, assign) BOOL isFirstSessionAfterUpgrade;
@property(nonatomic, assign) BOOL isFirstSessionAfterLanguageChange;
@property(nonatomic, assign) BOOL OSRestartedAfterPreviousSession;
@property(nonatomic, strong) NSString* OSVersion;
@property(nonatomic, strong) NSDate* sessionStartTime;
@property(nonatomic, strong) NSDate* sessionEndTime;
@property(nonatomic, assign) BOOL terminatedDuringSessionRestoration;
@property(nonatomic, strong) NSMutableSet<NSString*>* connectedSceneSessionsIDs;
@property(atomic, copy) NSDictionary<NSString*, NSString*>* reportParameters;
@property(nonatomic, assign) NSInteger memoryFootprint;
@property(nonatomic, assign) BOOL applicationWillTerminateWasReceived;
@property(nonatomic, assign) NSInteger tabCount;
@property(nonatomic, assign) NSInteger inactiveTabCount;
@property(nonatomic, assign) NSInteger OTRTabCount;
@property(atomic, strong) NSString* breadcrumbs;
@property(nonatomic, assign) NSInteger warmStartCount;

@end

@implementation PreviousSessionInfo {
  std::unique_ptr<UIApplicationState> _applicationState;
  base::RepeatingTimer _memoryFootprintUpdateTimer;
}

// Singleton PreviousSessionInfo.
static PreviousSessionInfo* gSharedInstance = nil;

+ (instancetype)sharedInstance {
  if (!gSharedInstance) {
    gSharedInstance = [[PreviousSessionInfo alloc] init];

    // Load the persisted information.
    NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];

    gSharedInstance->_applicationState.reset();
    if ([defaults objectForKey:previous_session_info_constants::
                                   kPreviousSessionInfoApplicationState]) {
      gSharedInstance->_applicationState = std::make_unique<UIApplicationState>(
          static_cast<UIApplicationState>([defaults
              integerForKey:previous_session_info_constants::
                                kPreviousSessionInfoApplicationState]));
    }

    gSharedInstance.availableDeviceStorage = -1;
    if ([defaults objectForKey:kPreviousSessionInfoAvailableDeviceStorage]) {
      gSharedInstance.availableDeviceStorage =
          [defaults integerForKey:kPreviousSessionInfoAvailableDeviceStorage];
    }
    gSharedInstance.didSeeMemoryWarningShortlyBeforeTerminating =
        [defaults boolForKey:previous_session_info_constants::
                                 kDidSeeMemoryWarningShortlyBeforeTerminating];
    gSharedInstance.deviceBatteryState = static_cast<DeviceBatteryState>(
        [defaults integerForKey:kPreviousSessionInfoBatteryState]);
    gSharedInstance.deviceBatteryLevel =
        [defaults floatForKey:kPreviousSessionInfoBatteryLevel];
    gSharedInstance.deviceThermalState = static_cast<DeviceThermalState>(
        [defaults integerForKey:kPreviousSessionInfoThermalState]);
    gSharedInstance.sessionStartTime =
        [defaults objectForKey:kPreviousSessionInfoStartTime];
    gSharedInstance.sessionEndTime =
        [defaults objectForKey:kPreviousSessionInfoEndTime];

    NSString* versionOfOSAtLastRun =
        [defaults stringForKey:kPreviousSessionInfoOSVersion];
    gSharedInstance.OSVersion = versionOfOSAtLastRun;

    NSString* lastRanVersion = [defaults stringForKey:kLastRanVersion];
    NSString* currentVersion =
        base::SysUTF8ToNSString(version_info::GetVersionNumber());
    gSharedInstance.isFirstSessionAfterUpgrade =
        ![lastRanVersion isEqualToString:currentVersion];

    gSharedInstance.connectedSceneSessionsIDs = [NSMutableSet
        setWithArray:[defaults
                         stringArrayForKey:
                             previous_session_info_constants::
                                 kPreviousSessionInfoConnectedSceneSessionIDs]];

    NSTimeInterval lastSystemStartTime =
        [defaults doubleForKey:previous_session_info_constants::kOSStartTime];

    gSharedInstance.OSRestartedAfterPreviousSession =
        // Allow 5 seconds variation to account for rounding error.
        (abs(lastSystemStartTime - GetOSStartTimeIntervalSinceReferenceDate()) >
         5) &&
        // Ensure that previous session actually exists.
        lastSystemStartTime;

    NSString* lastRanLanguage = [defaults stringForKey:kLastRanLanguage];
    NSString* currentLanguage = [[NSLocale preferredLanguages] objectAtIndex:0];
    gSharedInstance.isFirstSessionAfterLanguageChange =
        ![lastRanLanguage isEqualToString:currentLanguage];

    gSharedInstance.terminatedDuringSessionRestoration =
        [defaults boolForKey:previous_session_info_constants::
                                 kPreviousSessionInfoRestoringSession];

    NSMutableDictionary* reportParameters = [[NSMutableDictionary alloc] init];
    NSUInteger prefix_length = kPreviousSessionInfoParamsPrefix.length;
    for (NSString* key in [defaults dictionaryRepresentation].allKeys) {
      if ([key hasPrefix:kPreviousSessionInfoParamsPrefix]) {
        NSString* crash_key = [key substringFromIndex:prefix_length];
        reportParameters[crash_key] = [defaults stringForKey:key];
        [defaults removeObjectForKey:key];
      }
    }
    gSharedInstance.reportParameters = reportParameters;

    gSharedInstance.memoryFootprint =
        [defaults integerForKey:previous_session_info_constants::
                                    kPreviousSessionInfoMemoryFootprint];

    gSharedInstance.applicationWillTerminateWasReceived =
        [defaults boolForKey:kPreviousSessionInfoAppWillTerminate];
    gSharedInstance.tabCount =
        [defaults integerForKey:previous_session_info_constants::
                                    kPreviousSessionInfoTabCount];
    gSharedInstance.inactiveTabCount =
        [defaults integerForKey:previous_session_info_constants::
                                    kPreviousSessionInfoInactiveTabCount];
    gSharedInstance.OTRTabCount =
        [defaults integerForKey:previous_session_info_constants::
                                    kPreviousSessionInfoOTRTabCount];
    gSharedInstance.warmStartCount =
        [defaults integerForKey:previous_session_info_constants::
                                    kPreviousSessionInfoWarmStartCount];
  }
  return gSharedInstance;
}

+ (void)resetSharedInstanceForTesting {
  gSharedInstance = nil;
}

- (void)beginRecordingCurrentSession {
  if (self.didBeginRecordingCurrentSession)
    return;
  self.didBeginRecordingCurrentSession = YES;
  NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];

  // Set the current Chrome version.
  NSString* currentVersion =
      base::SysUTF8ToNSString(version_info::GetVersionNumber());
  [defaults setObject:currentVersion forKey:kLastRanVersion];

  // Set the current OS start time.
  [defaults setDouble:GetOSStartTimeIntervalSinceReferenceDate()
               forKey:previous_session_info_constants::kOSStartTime];

  // Set the current OS version.
  NSString* currentOSVersion =
      base::SysUTF8ToNSString(base::SysInfo::OperatingSystemVersion());
  [defaults setObject:currentOSVersion forKey:kPreviousSessionInfoOSVersion];

  // Set the current language.
  NSString* currentLanguage = [[NSLocale preferredLanguages] objectAtIndex:0];
  [defaults setObject:currentLanguage forKey:kLastRanLanguage];

  // Clear the memory warning flag.
  [defaults
      removeObjectForKey:previous_session_info_constants::
                             kDidSeeMemoryWarningShortlyBeforeTerminating];

  [[NSUserDefaults standardUserDefaults]
      removeObjectForKey:kPreviousSessionInfoAppWillTerminate];
  [[NSUserDefaults standardUserDefaults]
      removeObjectForKey:kPreviousSessionInfoAvailableDeviceStorage];

  [defaults setObject:[NSDate date] forKey:kPreviousSessionInfoStartTime];

  [[NSNotificationCenter defaultCenter]
      addObserver:self
         selector:@selector(updateApplicationState)
             name:UIApplicationDidEnterBackgroundNotification
           object:nil];
  [[NSNotificationCenter defaultCenter]
      addObserver:self
         selector:@selector(updateApplicationState)
             name:UIApplicationWillEnterForegroundNotification
           object:nil];
  [[NSNotificationCenter defaultCenter]
      addObserver:self
         selector:@selector(protectedDataWillBecomeUnavailable)
             name:UIApplicationProtectedDataWillBecomeUnavailable
           object:nil];
  [[NSNotificationCenter defaultCenter]
      addObserver:self
         selector:@selector(protectedDataDidBecomeAvailable)
             name:UIApplicationProtectedDataWillBecomeUnavailable
           object:nil];
  [[NSNotificationCenter defaultCenter]
      addObserver:self
         selector:@selector(updateApplicationState)
             name:UIApplicationDidBecomeActiveNotification
           object:nil];
  [[NSNotificationCenter defaultCenter]
      addObserver:self
         selector:@selector(updateApplicationState)
             name:UIApplicationWillResignActiveNotification
           object:nil];

  [UIDevice currentDevice].batteryMonitoringEnabled = YES;

  [[NSNotificationCenter defaultCenter]
      addObserver:self
         selector:@selector(updateStoredBatteryLevel)
             name:UIDeviceBatteryLevelDidChangeNotification
           object:nil];

  [[NSNotificationCenter defaultCenter]
      addObserver:self
         selector:@selector(updateStoredBatteryState)
             name:UIDeviceBatteryStateDidChangeNotification
           object:nil];

  [[NSNotificationCenter defaultCenter]
      addObserver:self
         selector:@selector(updateStoredThermalState)
             name:NSProcessInfoThermalStateDidChangeNotification
           object:nil];

  [[NSNotificationCenter defaultCenter]
      addObserver:self
         selector:@selector(applicationWillTerminate)
             name:UIApplicationWillTerminateNotification
           object:nil];

  [self resumeRecordingCurrentSession];
}

- (void)beginRecordingFieldTrials {
  _fieldTrialListObserver = std::make_unique<FieldTrialListObserverBridge>();
  bool success =
      base::FieldTrialList::AddObserver(_fieldTrialListObserver.get());
  DCHECK(success);
}

- (void)startRecordingMemoryFootprintWithInterval:(base::TimeDelta)interval {
  _memoryFootprintUpdateTimer.Start(FROM_HERE, interval, base::BindRepeating(^{
                                      [self updateMemoryFootprint];
                                    }));
}

- (void)stopRecordingMemoryFootprint {
  _memoryFootprintUpdateTimer.Stop();
}
- (void)resumeRecordingCurrentSession {
  if (self.recordingCurrentSession)
    return;
  self.recordingCurrentSession = YES;
  [self updateApplicationState];
  [self updateStoredBatteryLevel];
  [self updateStoredBatteryState];
  [self updateStoredThermalState];
  // Save critical state information for crash detection.
  [[NSUserDefaults standardUserDefaults] synchronize];
}

- (void)pauseRecordingCurrentSession {
  self.recordingCurrentSession = NO;
}

- (void)protectedDataWillBecomeUnavailable {
  [self pauseRecordingCurrentSession];
}

- (void)protectedDataDidBecomeAvailable {
  [self resumeRecordingCurrentSession];
}

- (UIApplicationState*)applicationState {
  return _applicationState.get();
}

- (void)updateSessionEndTime {
  if (!self.recordingCurrentSession)
    return;
  [[NSUserDefaults standardUserDefaults] setObject:[NSDate date]
                                            forKey:kPreviousSessionInfoEndTime];
}

- (void)updateStoredBatteryLevel {
  if (!self.recordingCurrentSession)
    return;
  [[NSUserDefaults standardUserDefaults]
      setFloat:[UIDevice currentDevice].batteryLevel
        forKey:kPreviousSessionInfoBatteryLevel];
  [self updateSessionEndTime];
}

- (void)updateApplicationState {
  if (!self.recordingCurrentSession)
    return;
  [[NSUserDefaults standardUserDefaults]
      setInteger:UIApplication.sharedApplication.applicationState
          forKey:previous_session_info_constants::
                     kPreviousSessionInfoApplicationState];

  [self updateSessionEndTime];
}

- (void)updateStoredBatteryState {
  if (!self.recordingCurrentSession)
    return;
  UIDevice* device = [UIDevice currentDevice];
  // Translate value to an app defined enum as the system could change the
  // underlying values of UIDeviceBatteryState between OS versions.
  DeviceBatteryState batteryState =
      GetBatteryStateFromUIDeviceBatteryState(device.batteryState);
  NSInteger batteryStateValue =
      static_cast<std::underlying_type<DeviceBatteryState>::type>(batteryState);

  [[NSUserDefaults standardUserDefaults]
      setInteger:batteryStateValue
          forKey:kPreviousSessionInfoBatteryState];

  [self updateSessionEndTime];
}

- (void)updateStoredThermalState {
  if (!self.recordingCurrentSession)
    return;
  NSProcessInfo* processInfo = [NSProcessInfo processInfo];
  // Translate value to an app defined enum as the system could change the
  // underlying values of NSProcessInfoThermalState between OS versions.
  DeviceThermalState thermalState =
      GetThermalStateFromNSProcessInfoThermalState([processInfo thermalState]);
  NSInteger thermalStateValue =
      static_cast<std::underlying_type<DeviceThermalState>::type>(thermalState);

  [[NSUserDefaults standardUserDefaults]
      setInteger:thermalStateValue
          forKey:kPreviousSessionInfoThermalState];

  [self updateSessionEndTime];
}

- (void)applicationWillTerminate {
  [NSUserDefaults.standardUserDefaults
      setBool:YES
       forKey:kPreviousSessionInfoAppWillTerminate];
  [NSUserDefaults.standardUserDefaults synchronize];
}

- (void)updateMemoryFootprint {
  if (!self.recordingCurrentSession)
    return;

  task_vm_info taskInfoData;
  mach_msg_type_number_t count = sizeof(task_vm_info) / sizeof(natural_t);
  kern_return_t result =
      task_info(mach_task_self(), TASK_VM_INFO,
                reinterpret_cast<task_info_t>(&taskInfoData), &count);
  if (result == KERN_SUCCESS) {
    [NSUserDefaults.standardUserDefaults
        setInteger:taskInfoData.phys_footprint
            forKey:previous_session_info_constants::
                       kPreviousSessionInfoMemoryFootprint];
    [self updateSessionEndTime];
  }
}

- (void)setMemoryWarningFlag {
  if (!self.didBeginRecordingCurrentSession)
    return;

  NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
  [defaults setBool:YES
             forKey:previous_session_info_constants::
                        kDidSeeMemoryWarningShortlyBeforeTerminating];
  // Save critical state information for crash detection.
  [defaults synchronize];
}

- (void)resetMemoryWarningFlag {
  if (!self.didBeginRecordingCurrentSession)
    return;

  NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
  [defaults
      removeObjectForKey:previous_session_info_constants::
                             kDidSeeMemoryWarningShortlyBeforeTerminating];
  // Save critical state information for crash detection.
  [defaults synchronize];
}

- (void)synchronizeSceneSessionIDs {
  NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
  [defaults setObject:[self.connectedSceneSessionsIDs allObjects]
               forKey:previous_session_info_constants::
                          kPreviousSessionInfoConnectedSceneSessionIDs];
  [defaults synchronize];
}

- (void)addSceneSessionID:(NSString*)sessionID {
  [self.connectedSceneSessionsIDs addObject:sessionID];
  [self synchronizeSceneSessionIDs];
}

- (void)removeSceneSessionID:(NSString*)sessionID {
  [self.connectedSceneSessionsIDs removeObject:sessionID];
  [self synchronizeSceneSessionIDs];
}

- (void)resetConnectedSceneSessionIDs {
  self.connectedSceneSessionsIDs = [[NSMutableSet alloc] init];
  [self synchronizeSceneSessionIDs];
}

- (void)incrementWarmStartCount {
  NSUserDefaults* defaults = NSUserDefaults.standardUserDefaults;
  NSInteger warmStartCount =
      [defaults integerForKey:previous_session_info_constants::
                                  kPreviousSessionInfoWarmStartCount];
  [defaults setInteger:warmStartCount + 1
                forKey:previous_session_info_constants::
                           kPreviousSessionInfoWarmStartCount];
  [defaults synchronize];
}

- (void)resetWarmStartCount {
  [NSUserDefaults.standardUserDefaults
      setInteger:0
          forKey:previous_session_info_constants::
                     kPreviousSessionInfoWarmStartCount];
  [NSUserDefaults.standardUserDefaults synchronize];
}

- (base::ScopedClosureRunner)startSessionRestoration {
  if (self.numberOfSessionsBeingRestored == 0) {
    [NSUserDefaults.standardUserDefaults
        setBool:YES
         forKey:previous_session_info_constants::
                    kPreviousSessionInfoRestoringSession];
    // Save critical state information for crash detection.
    [NSUserDefaults.standardUserDefaults synchronize];
  }
  ++self.numberOfSessionsBeingRestored;

  return base::ScopedClosureRunner(base::BindOnce(^{
    --self.numberOfSessionsBeingRestored;
    if (self.numberOfSessionsBeingRestored == 0) {
      [self resetSessionRestorationFlag];
    }
  }));
}

- (void)resetSessionRestorationFlag {
  gSharedInstance.terminatedDuringSessionRestoration = NO;
  [NSUserDefaults.standardUserDefaults
      removeObjectForKey:previous_session_info_constants::
                             kPreviousSessionInfoRestoringSession];
  // Save critical state information for crash detection.
  [NSUserDefaults.standardUserDefaults synchronize];
}

- (void)updateCurrentSessionTabCount:(NSInteger)count {
  [NSUserDefaults.standardUserDefaults
      setInteger:count
          forKey:previous_session_info_constants::kPreviousSessionInfoTabCount];
  [NSUserDefaults.standardUserDefaults synchronize];
}

- (void)updateCurrentSessionInactiveTabCount:(NSInteger)count {
  [NSUserDefaults.standardUserDefaults
      setInteger:count
          forKey:previous_session_info_constants::
                     kPreviousSessionInfoInactiveTabCount];
  [NSUserDefaults.standardUserDefaults synchronize];
}

- (void)updateCurrentSessionOTRTabCount:(NSInteger)count {
  [NSUserDefaults.standardUserDefaults
      setInteger:count
          forKey:previous_session_info_constants::
                     kPreviousSessionInfoOTRTabCount];
  [NSUserDefaults.standardUserDefaults synchronize];
}

- (void)setBreadcrumbsLog:(NSString*)breadcrumbs {
  gSharedInstance.breadcrumbs = breadcrumbs;
}

- (void)setReportParameterValue:(NSString*)value forKey:(NSString*)key {
  if (![NSThread isMainThread]) {
    dispatch_async(dispatch_get_main_queue(), ^{
      [self setReportParameterValue:value forKey:key];
    });
    return;
  }
  DCHECK([NSThread isMainThread]);
  // Previously this logic would read and write an NSDictionary, but it lead to
  // crashes within the NSUserDefaults logic. Instead, write a separate defaults
  // entry for each key.
  [NSUserDefaults.standardUserDefaults setObject:value
                                          forKey:ReportParamKey(key)];
  [NSUserDefaults.standardUserDefaults synchronize];
}

- (void)setReportParameterURL:(const GURL&)URL forKey:(NSString*)key {
  // Store only URL origin (not whole URL spec) as requested by Privacy Team.
  [self
      setReportParameterValue:base::SysUTF8ToNSString(
                                  URL.DeprecatedGetOriginAsURL().spec().c_str())
                       forKey:key];
}

- (void)removeReportParameterForKey:(NSString*)key {
  if (![NSThread isMainThread]) {
    dispatch_async(dispatch_get_main_queue(), ^{
      [self removeReportParameterForKey:key];
    });
    return;
  }
  DCHECK([NSThread isMainThread]);
  [NSUserDefaults.standardUserDefaults removeObjectForKey:ReportParamKey(key)];
  [NSUserDefaults.standardUserDefaults synchronize];
}

@end