// Copyright 2015 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. #import "components/previous_session_info/previous_session_info.h" #include #import #include "base/ios/ios_util.h" #include "base/strings/sys_string_conversions.h" #include "base/system/sys_info.h" #include "base/time/time.h" #include "base/timer/timer.h" #import "components/previous_session_info/previous_session_info_private.h" #include "components/version_info/version_info.h" #if !defined(__has_feature) || !__has_feature(objc_arc) #error "This file requires ARC support." #endif using previous_session_info_constants::DeviceBatteryState; using previous_session_info_constants::DeviceThermalState; 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"; // TODO(crbug.com/1266034): Remove key for no longer logged state. // - A (boolean) describing whether or not low power mode is enabled. NSString* const kPreviousSessionInfoLowPowerMode = @"PreviousSessionInfoLowPowerMode"; // TODO(crbug.com/1295763): Remove key for no longer used state. // - A (boolean) describing whether the last session was on Multi-Window enabled // version of the application. NSString* const kPreviousSessionInfoMultiWindowEnabled = @"PreviousSessionInfoMultiWindowEnabled"; // - A (boolean) describing whether the last session received // ApplicationWillTerminate Notification. NSString* const kPreviousSessionInfoAppWillTerminate = @"PreviousSessionInfoAppWillTerminate"; } // 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 kPreviousSessionInfoParams = @"PreviousSessionInfoParams"; NSString* const kPreviousSessionInfoMemoryFootprint = @"PreviousSessionInfoMemoryFootprint"; NSString* const kPreviousSessionInfoTabCount = @"PreviousSessionInfoTabCount"; NSString* const kPreviousSessionInfoOTRTabCount = @"PreviousSessionInfoOTRTabCount"; } // namespace previous_session_info_constants @interface PreviousSessionInfo () // 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* connectedSceneSessionsIDs; @property(nonatomic, copy) NSDictionary* reportParameters; @property(nonatomic, assign) NSInteger memoryFootprint; @property(nonatomic, assign) BOOL applicationWillTerminateWasReceived; @property(nonatomic, assign) NSInteger tabCount; @property(nonatomic, assign) NSInteger OTRTabCount; @end @implementation PreviousSessionInfo { std::unique_ptr _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( static_cast([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( [defaults integerForKey:kPreviousSessionInfoBatteryState]); gSharedInstance.deviceBatteryLevel = [defaults floatForKey:kPreviousSessionInfoBatteryLevel]; gSharedInstance.deviceThermalState = static_cast( [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]; // This key is no longer being used so we remove it here, // but this should be cleaned-up in many milestones. // TODO(crbug.com/1295763): Remove this line. [[NSUserDefaults standardUserDefaults] removeObjectForKey:kPreviousSessionInfoMultiWindowEnabled]; 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]; gSharedInstance.reportParameters = [defaults dictionaryForKey:previous_session_info_constants:: kPreviousSessionInfoParams]; gSharedInstance.memoryFootprint = [defaults integerForKey:previous_session_info_constants:: kPreviousSessionInfoMemoryFootprint]; gSharedInstance.applicationWillTerminateWasReceived = [defaults boolForKey:kPreviousSessionInfoAppWillTerminate]; gSharedInstance.tabCount = [defaults integerForKey:previous_session_info_constants:: kPreviousSessionInfoTabCount]; gSharedInstance.OTRTabCount = [defaults integerForKey:previous_session_info_constants:: kPreviousSessionInfoOTRTabCount]; } 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:kPreviousSessionInfoLowPowerMode]; [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)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)updateAvailableDeviceStorage:(NSInteger)availableStorage { if (!self.recordingCurrentSession) return; [[NSUserDefaults standardUserDefaults] setInteger:availableStorage forKey:kPreviousSessionInfoAvailableDeviceStorage]; [self updateSessionEndTime]; } - (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::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::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(&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]; } - (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)updateCurrentSessionOTRTabCount:(NSInteger)count { [NSUserDefaults.standardUserDefaults setInteger:count forKey:previous_session_info_constants:: kPreviousSessionInfoOTRTabCount]; [NSUserDefaults.standardUserDefaults synchronize]; } - (void)setReportParameterValue:(NSString*)value forKey:(NSString*)key { NSMutableDictionary* params = [[NSUserDefaults.standardUserDefaults dictionaryForKey:previous_session_info_constants:: kPreviousSessionInfoParams] mutableCopy]; if (!params) { params = [NSMutableDictionary dictionaryWithCapacity:1]; } params[key] = value; [NSUserDefaults.standardUserDefaults setObject:params forKey:previous_session_info_constants::kPreviousSessionInfoParams]; [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 { NSMutableDictionary* URLs = [[NSUserDefaults.standardUserDefaults dictionaryForKey:previous_session_info_constants:: kPreviousSessionInfoParams] mutableCopy]; if (URLs) { URLs[key] = nil; if (URLs.count == 0) { URLs = nil; } [NSUserDefaults.standardUserDefaults setObject:URLs forKey:previous_session_info_constants::kPreviousSessionInfoParams]; [NSUserDefaults.standardUserDefaults synchronize]; } } @end