[go: nahoru, domu]

blob: 1d7340ad57dc2678f10776e592f678ba9aab4eb7 [file] [log] [blame]
// Copyright 2022 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/variations_app_state_agent.h"
#import "ios/chrome/app/variations_app_state_agent+testing.h"
#import "base/apple/foundation_util.h"
#import "base/metrics/field_trial.h"
#import "base/metrics/histogram_functions.h"
#import "base/rand_util.h"
#import "base/time/time.h"
#import "components/prefs/pref_registry_simple.h"
#import "components/prefs/pref_service.h"
#import "components/variations/service/variations_field_trial_creator.h"
#import "components/variations/service/variations_service_utils.h"
#import "components/variations/variations_seed_store.h"
#import "components/version_info/version_info.h"
#import "ios/chrome/app/application_delegate/app_state.h"
#import "ios/chrome/app/application_delegate/startup_information.h"
#import "ios/chrome/app/launch_screen_view_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/ui/first_run/first_run_util.h"
#import "ios/chrome/browser/variations/ios_chrome_variations_seed_fetcher.h"
#import "ios/chrome/browser/variations/ios_chrome_variations_seed_store.h"
#import "ios/chrome/common/channel_info.h"
// Name of trial and experiment groups.
const char kIOSChromeVariationsTrialName[] = "kIOSChromeVariationsTrial";
const char kIOSChromeVariationsTrialDefaultGroup[] = "Default";
const char kIOSChromeVariationsTrialControlGroup[] = "Control-v1";
const char kIOSChromeVariationsTrialEnabledGroup[] = "Enabled-v1";
// Histogram name for seed expiry.
const char kIOSSeedExpiryHistogram[] = "IOS.Variations.CreateTrials.SeedExpiry";
namespace {
using ::variations::HasSeedExpiredSinceTime;
using ::variations::SeedApplicationStage;
using ::variations::VariationsSeedExpiry;
using ::variations::VariationsSeedStore;
using ::version_info::Channel;
// The NSUserDefault key to store the time the last seed is fetched.
NSString* kLastVariationsSeedFetchTimeKey = @"kLastVariationsSeedFetchTime";
// Local state key of experiment group assigned, persisted for subsequent runs.
const char kFirstRunSeedFetchExperimentGroupPref[] = "ios.variations.first_run";
// Experiment group for the iOS variations trial. It will correspond to the
// trial group activated in the respective FieldTrial object, once the latter is
// setup.
enum class IOSChromeVariationsGroup {
kNotAssigned = 0,
kNotFirstRun,
kDefault,
kControl,
kEnabled,
};
#pragma mark - Helpers
// Group weight for both enabled and control experiment groups.
// NOTE: The value will be updated during the incremental rollout period.
int GetGroupWeight() {
switch (GetChannel()) {
case Channel::UNKNOWN:
case Channel::CANARY:
case Channel::DEV:
case Channel::BETA:
return 50;
case Channel::STABLE:
return 50;
}
}
// Returns the fetch time of the variations seed store fetched by a previous
// run, and null if such seed doesn't exist.
base::Time GetLastVariationsSeedFetchTime() {
double timestamp = [[NSUserDefaults standardUserDefaults]
doubleForKey:kLastVariationsSeedFetchTimeKey];
return base::Time::FromDoubleT(timestamp);
}
// Records metric for `kIOSSeedExpiryHistogram` according whether there is a
// seed in the variations seed store fetched by a previous run, and if there is,
// whether it is expired.
void RecordSeedExpiry(base::Time time) {
VariationsSeedExpiry expiry;
if (time.is_null()) {
expiry = VariationsSeedExpiry::kFetchTimeMissing;
} else if (HasSeedExpiredSinceTime(time)) {
expiry = VariationsSeedExpiry::kExpired;
} else {
expiry = VariationsSeedExpiry::kNotExpired;
}
base::UmaHistogramEnumeration(kIOSSeedExpiryHistogram, expiry);
}
// Creates and returns a one-time randomized trial group assignment with regards
// to given group weights.
// NOTE: `enabled_weight` and `control_weight` should be the same unless
// overriden by test cases.
IOSChromeVariationsGroup CreateOneTimeExperimentGroupAssignment(
int enabled_weight,
int control_weight) {
DCHECK_LE(enabled_weight + control_weight, 100);
double rand = base::RandDouble() * 100;
if (rand < enabled_weight) {
return IOSChromeVariationsGroup::kEnabled;
} else if (rand < enabled_weight + control_weight) {
return IOSChromeVariationsGroup::kControl;
} else {
return IOSChromeVariationsGroup::kDefault;
}
}
// Creates and activates the client side field trial. First run users would be
// assigned to a group that corresponds to the parameter `group`, while others
// would be assigned to their previous groups in the same version of the
// experiment, if exists, or be excluded out of the experiment. This is called
// when local state is ready, and will save the field trial group name in the
// local state as well.
void ActivateFieldTrialForGroup(IOSChromeVariationsGroup group) {
// Check if the group name exists in the local state.
// This is to cover the scenario when the app has been previously installed
// but crashes before first run experience completes. In this case, the seed
// would be fetched but not used by variations service, and so the field trial
// group would be assigned to the previous one.
PrefService* local_state = GetApplicationContext()->GetLocalState();
std::string group_name;
switch (group) {
case IOSChromeVariationsGroup::kNotAssigned:
NOTREACHED();
break;
case IOSChromeVariationsGroup::kNotFirstRun:
// First run completed before the experiment is setup. Use group
// name from previous launches if exists, or leave empty if not.
group_name =
local_state->GetString(kFirstRunSeedFetchExperimentGroupPref);
break;
case IOSChromeVariationsGroup::kEnabled:
group_name = kIOSChromeVariationsTrialEnabledGroup;
break;
case IOSChromeVariationsGroup::kControl:
group_name = kIOSChromeVariationsTrialControlGroup;
break;
case IOSChromeVariationsGroup::kDefault:
group_name = kIOSChromeVariationsTrialDefaultGroup;
break;
}
local_state->SetString(kFirstRunSeedFetchExperimentGroupPref, group_name);
if (!group_name.empty()) {
base::FieldTrial* trial = base::FieldTrialList::CreateFieldTrial(
kIOSChromeVariationsTrialName, group_name);
trial->Activate();
}
}
// Retrieves the time the last variations seed is fetched from local state, and
// stores it into NSUserDefaults. It should be executed every time before the
// app shuts down, so the value could be used for the next startup, before
// PrefService is instantiated.
void SaveFetchTimeOfLatestSeedInLocalState() {
PrefService* local_state = GetApplicationContext()->GetLocalState();
const base::Time seed_fetch_time =
variations::VariationsSeedStore::GetLastFetchTimeFromPrefService(
local_state);
if (!seed_fetch_time.is_null()) {
[[NSUserDefaults standardUserDefaults]
setDouble:seed_fetch_time.ToDoubleT()
forKey:kLastVariationsSeedFetchTimeKey];
}
}
} // namespace
#pragma mark - VariationsAppStateAgent
@interface VariationsAppStateAgent () <IOSChromeVariationsSeedFetcherDelegate> {
// Caches the previous activation level.
SceneActivationLevel _previousActivationLevel;
// Whether the variations seed fetch has completed.
BOOL _seedFetchCompleted;
// Whether the extended launch screen is shown.
BOOL _extendedLaunchScreenShown;
// The fetcher object used to fetch the seed.
IOSChromeVariationsSeedFetcher* _fetcher;
// Group assignment of the iOS variations trial.
IOSChromeVariationsGroup _group;
}
@end
@implementation VariationsAppStateAgent
- (instancetype)init {
int groupWeight = GetGroupWeight();
DCHECK_LE(groupWeight, 50);
// Note: `ShouldPresentFirstRunExperience()` will return YES as long as the
// user has not completed a first run experience.
return [self
initWithFirstRunExperience:ShouldPresentFirstRunExperience()
lastSeedFetchTime:GetLastVariationsSeedFetchTime()
fetcher:[[IOSChromeVariationsSeedFetcher alloc] init]
enabledGroupWeight:groupWeight
controlGroupWeight:groupWeight];
}
- (instancetype)initWithFirstRunExperience:(BOOL)shouldPresentFRE
lastSeedFetchTime:(base::Time)lastSeedFetchTime
fetcher:
(IOSChromeVariationsSeedFetcher*)fetcher
enabledGroupWeight:(int)enabledGroupWeight
controlGroupWeight:(int)controlGroupWeight {
DCHECK_LE(enabledGroupWeight + controlGroupWeight, 100);
self = [super init];
if (self) {
_previousActivationLevel = SceneActivationLevelUnattached;
_seedFetchCompleted = NO;
_extendedLaunchScreenShown = NO;
// By checking last fetch time from NSUserDefaults, `firstRunStatus` covers
// the scenario when a user relaunches after existing the app during FRE;
// however, if the app crashes during FRE, the value will still be YES in
// the subsequent launch.
// TODO(crbug.com/1372180): Import crash helper and take into account
// previous crash statistics into account.
BOOL firstRun = shouldPresentFRE && lastSeedFetchTime.is_null();
_group = firstRun ? CreateOneTimeExperimentGroupAssignment(
enabledGroupWeight, controlGroupWeight)
: IOSChromeVariationsGroup::kNotFirstRun;
RecordSeedExpiry(lastSeedFetchTime);
if (_group == IOSChromeVariationsGroup::kEnabled) {
_fetcher = fetcher;
_fetcher.delegate = self;
[_fetcher startSeedFetch];
}
}
return self;
}
+ (void)registerLocalState:(PrefRegistrySimple*)registry {
registry->RegisterStringPref(kFirstRunSeedFetchExperimentGroupPref,
std::string());
}
#pragma mark - AppAgentObserver
- (void)appState:(AppState*)appState
willTransitionToInitStage:(InitStage)nextInitStage {
if (self.appState.initStage == InitStageBrowserObjectsForBackgroundHandlers) {
// Records whether the fetched seed for first run has been applied, and if
// not, which stage has the seed application process reached.
//
// Note that this check is used to makes sure this metric only gets logged
// on first run, so that subsequent runs in the `Enabled` group would not
// contaminate the data. There is NO field trial group for `kNotFirstRun`.
if (_group != IOSChromeVariationsGroup::kNotFirstRun) {
base::UmaHistogramEnumeration(
"IOS.Variations.FirstRun.SeedApplicationStage",
[IOSChromeVariationsSeedStore seedApplicationStage]);
}
ActivateFieldTrialForGroup(_group);
}
}
#pragma mark - ObservingAppAgent
- (void)appState:(AppState*)appState
didTransitionFromInitStage:(InitStage)previousInitStage {
if (self.appState.initStage == InitStageVariationsSeed) {
// Keep waiting for the seed if the app should have variations seed fetched
// but hasn't.
if (_group != IOSChromeVariationsGroup::kEnabled || _seedFetchCompleted) {
[self.appState queueTransitionToNextInitStage];
}
}
[super appState:appState didTransitionFromInitStage:previousInitStage];
}
- (void)sceneState:(SceneState*)sceneState
transitionedToActivationLevel:(SceneActivationLevel)level {
// If the app would be showing UI before Chrome UI is ready, extend the launch
// screen.
if (self.appState.initStage == InitStageVariationsSeed &&
_group == IOSChromeVariationsGroup::kEnabled &&
level > SceneActivationLevelBackground && !_extendedLaunchScreenShown) {
[self showExtendedLaunchScreen:sceneState];
_extendedLaunchScreenShown = YES;
}
// Saves the fetch time to NSUserDefaults when the app moves from foreground
// to background.
if (_previousActivationLevel > SceneActivationLevelBackground &&
level == SceneActivationLevelBackground &&
self.appState.initStage > InitStageBrowserObjectsForBackgroundHandlers) {
SaveFetchTimeOfLatestSeedInLocalState();
}
_previousActivationLevel = level;
[super sceneState:sceneState transitionedToActivationLevel:level];
}
#pragma mark - IOSChromeVariationsSeedFetcherDelegate
- (void)variationsSeedFetcherDidCompleteFetchWithSuccess:(BOOL)success {
DCHECK_EQ(_group, IOSChromeVariationsGroup::kEnabled);
DCHECK_LE(self.appState.initStage, InitStageVariationsSeed);
_seedFetchCompleted = YES;
_fetcher.delegate = nil;
if (self.appState.initStage == InitStageVariationsSeed) {
[self.appState queueTransitionToNextInitStage];
}
}
#pragma mark - private
// Show a view that mocks the launch screen. This should only be called when the
// scene will be active on the foreground but the seed has not been fetched to
// initialize Chrome.
- (void)showExtendedLaunchScreen:(SceneState*)sceneState {
DCHECK(sceneState.window);
// Set up view controller.
UIViewController* controller = [[LaunchScreenViewController alloc] init];
[sceneState.window setRootViewController:controller];
[sceneState.window makeKeyAndVisible];
}
@end