// Copyright 2020 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/app/app_metrics_app_state_agent.h"
#import "base/metrics/histogram_macros.h"
#import "base/time/time.h"
#import "ios/chrome/app/application_delegate/app_state.h"
#import "ios/chrome/app/application_delegate/metrics_mediator.h"
#import "ios/chrome/app/application_delegate/startup_information.h"
#import "ios/chrome/app/deferred_initialization_runner.h"
#import "ios/chrome/app/tests_hook.h"
#import "ios/chrome/browser/metrics/model/ios_profile_session_durations_service.h"
#import "ios/chrome/browser/metrics/model/ios_profile_session_durations_service_factory.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_controller.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_state.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/profile/profile_manager_ios.h"
#import "ios/public/provider/chrome/browser/primes/primes_api.h"
namespace {
// Constant for deferring snapshotting startup memory usage
NSString* const kTakeStartupMemorySnapshot = @"TakeStartupMemorySnapshot";
// Constant for naming the startup memory snapshot
NSString* const kDeferredInitializationBlocksComplete =
@"DeferredInitializationBlocksComplete";
} // namespace
@interface AppMetricsAppStateAgent () <SceneStateObserver>
// Observed app state.
@property(nonatomic, weak) AppState* appState;
// This flag is set when the first scene has activated since the startup, and
// never reset during the app's lifetime.
@property(nonatomic, assign) BOOL firstSceneHasActivated;
// This flag is set when the first scene has connected since the startup, and
// never reset during the app's lifetime.
@property(nonatomic, assign) BOOL firstSceneHasConnected;
@end
@implementation AppMetricsAppStateAgent
#pragma mark - AppStateAgent
- (void)setAppState:(AppState*)appState {
// This should only be called once!
DCHECK(!_appState);
_appState = appState;
[appState addObserver:self];
}
#pragma mark - AppStateObserver
- (void)appState:(AppState*)appState sceneConnected:(SceneState*)sceneState {
[sceneState addObserver:self];
}
- (void)appState:(AppState*)appState
didTransitionFromInitStage:(InitStage)previousInitStage {
if (appState.initStage == InitStageBrowserObjectsForBackgroundHandlers) {
// Log session start if the app is already foreground.
if (self.appState.foregroundScenes.count > 0) {
[self handleSessionStart];
}
}
}
#pragma mark - SceneStateObserver
- (void)sceneState:(SceneState*)sceneState
transitionedToActivationLevel:(SceneActivationLevel)level {
if (!self.firstSceneHasConnected) {
self.appState.startupInformation.firstSceneConnectionTime =
base::TimeTicks::Now();
self.firstSceneHasConnected = YES;
if (self.appState.initStage >=
InitStageBrowserObjectsForBackgroundHandlers) {
[MetricsMediator createStartupTrackingTask];
}
}
if (self.appState.initStage < InitStageBrowserObjectsForBackgroundHandlers) {
return;
}
if (level >= SceneActivationLevelForegroundInactive &&
self.appState.lastTimeInForeground.is_null()) {
[self handleSessionStart];
} else if (level <= SceneActivationLevelBackground) {
// Do not consider the app as brackgrounded when there are still scenes on
// the foreground.
if (self.appState.foregroundScenes.count > 0) {
return;
}
if (self.appState.lastTimeInForeground.is_null()) {
// This method will be called multiple times, once per scene, if multiple
// scenes go background simulatneously (for example, if two windows were
// in split screen and the user swiped to go home). Only log the session
// duration once. This also makes sure that the first scene that ramps up
// to foreground doesn't end the session.
return;
}
[self handleSessionEnd];
DCHECK(self.appState.lastTimeInForeground.is_null());
}
if (level >= SceneActivationLevelForegroundActive) {
if (!self.firstSceneHasActivated) {
self.firstSceneHasActivated = YES;
[MetricsMediator logStartupDuration:self.appState.startupInformation];
if (ios::provider::IsPrimesSupported()) {
ios::provider::PrimesAppReady();
[[DeferredInitializationRunner sharedInstance]
enqueueBlockNamed:kTakeStartupMemorySnapshot
block:^{
ios::provider::PrimesTakeMemorySnapshot(
kDeferredInitializationBlocksComplete);
tests_hook::SignalAppLaunched();
}];
}
}
}
}
#pragma mark - private
- (void)handleSessionStart {
self.appState.lastTimeInForeground = base::TimeTicks::Now();
for (ChromeBrowserState* browserState :
GetApplicationContext()->GetProfileManager()->GetLoadedProfiles()) {
IOSProfileSessionDurationsService* psdService =
IOSProfileSessionDurationsServiceFactory::GetForBrowserState(
browserState);
if (psdService) {
psdService->OnSessionStarted(self.appState.lastTimeInForeground);
}
}
}
- (void)handleSessionEnd {
DCHECK(!self.appState.lastTimeInForeground.is_null());
base::TimeDelta duration =
base::TimeTicks::Now() - self.appState.lastTimeInForeground;
UMA_HISTOGRAM_LONG_TIMES("Session.TotalDuration", duration);
UMA_HISTOGRAM_CUSTOM_TIMES("Session.TotalDurationMax1Day", duration,
base::Milliseconds(1), base::Hours(24), 50);
for (ChromeBrowserState* browserState :
GetApplicationContext()->GetProfileManager()->GetLoadedProfiles()) {
IOSProfileSessionDurationsService* psdService =
IOSProfileSessionDurationsServiceFactory::GetForBrowserState(
browserState);
if (psdService) {
psdService->OnSessionEnded(duration);
}
self.appState.lastTimeInForeground = base::TimeTicks();
}
}
@end