// 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/browser/default_browser/model/utils.h"
#import <UIKit/UIKit.h>
#import "base/apple/foundation_util.h"
#import "base/command_line.h"
#import "base/ios/ios_util.h"
#import "base/metrics/field_trial_params.h"
#import "base/metrics/histogram_functions.h"
#import "base/metrics/user_metrics.h"
#import "base/notreached.h"
#import "base/strings/strcat.h"
#import "base/strings/string_number_conversions.h"
#import "base/time/time.h"
#import "components/feature_engagement/public/event_constants.h"
#import "components/feature_engagement/public/tracker.h"
#import "components/prefs/pref_service.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/signin/model/signin_util.h"
// Key in NSUserDefaults containing an NSDictionary used to store all the
// information.
extern NSString* const kDefaultBrowserUtilsKey;
namespace {
// Key in storage containing an array of dates. Each date correspond to
// a general event of interest for Default Browser Promo modals.
NSString* const kLastSignificantUserEventGeneral = @"lastSignificantUserEvent";
// Key in storage containing an array of dates. Each date correspond to
// a made for iOS event of interest for Default Browser Promo modals.
NSString* const kLastSignificantUserEventMadeForIOS =
@"lastSignificantUserEventMadeForIOS";
// Key in storage containing an array of dates. Each date correspond to
// an all tabs event of interest for Default Browser Promo modals.
NSString* const kLastSignificantUserEventAllTabs =
@"lastSignificantUserEventAllTabs";
// Key in storage containing an int indicating the number of times the
// user has interacted with a non-modal promo.
NSString* const kUserInteractedWithNonModalPromoCount =
@"userInteractedWithNonModalPromoCount";
// Action string for "Appear" event of the promo.
const char kAppearAction[] = "Appear";
// Maximum number of past event timestamps to record.
const size_t kMaxPastTimestampsToRecord = 10;
// Maximum number of past event timestamps to record for trigger criteria
// experiment.
const size_t kMaxPastTimestampsToRecordForTriggerCriteriaExperiment = 50;
// Time threshold before activity timestamps should be removed.
constexpr base::TimeDelta kUserActivityTimestampExpiration = base::Days(21);
// Time threshold for the last URL open before no URL opens likely indicates
// Chrome is no longer the default browser.
constexpr base::TimeDelta kLatestURLOpenForDefaultBrowser = base::Days(21);
// Cool down between fullscreen promos.
constexpr base::TimeDelta kFullscreenPromoCoolDown = base::Days(14);
// Short cool down between promos.
constexpr base::TimeDelta kPromosShortCoolDown = base::Days(3);
// Time threshold for default browser trigger criteria experiment statistics.
constexpr base::TimeDelta kTriggerCriteriaExperimentStatExpiration =
base::Days(14);
// Returns maximum number of past event timestamps to record.
size_t GetMaxPastTimestampsToRecord() {
if (IsDefaultBrowserTriggerCriteraExperimentEnabled()) {
return kMaxPastTimestampsToRecordForTriggerCriteriaExperiment;
}
return kMaxPastTimestampsToRecord;
}
// Creates storage object from legacy keys.
NSMutableDictionary<NSString*, NSObject*>* CreateStorageObjectFromLegacyKeys() {
NSMutableDictionary<NSString*, NSObject*>* dictionary =
[[NSMutableDictionary alloc] init];
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
for (NSString* key in DefaultBrowserUtilsLegacyKeysForTesting()) {
NSObject* object = [defaults objectForKey:key];
if (object) {
dictionary[key] = object;
[defaults removeObjectForKey:key];
}
}
return dictionary;
}
// Helper function to get the data for `key` from the storage object.
template <typename T>
T* GetObjectFromStorageForKey(NSString* key) {
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
NSDictionary<NSString*, NSObject*>* storage =
[defaults objectForKey:kDefaultBrowserUtilsKey];
// If the storage is missing, create it, possibly from the legacy keys.
// This is used to support loading data written by version 109 or ealier.
// Remove once migrating data from such old version is no longer supported.
if (!storage) {
storage = CreateStorageObjectFromLegacyKeys();
[defaults setObject:storage forKey:kDefaultBrowserUtilsKey];
}
DCHECK(storage);
return base::apple::ObjCCast<T>(storage[key]);
}
// Helper function to update storage with `dict`. If a key in `dict` maps
// to `NSNull` instance, it will be removed from storage.
void UpdateStorageWithDictionary(NSDictionary<NSString*, NSObject*>* dict) {
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
NSMutableDictionary<NSString*, NSObject*>* storage =
[[defaults objectForKey:kDefaultBrowserUtilsKey] mutableCopy];
// If the storage is missing, create it, possibly from the legacy keys.
// This is used to support loading data written by version 109 or ealier.
// Remove once migrating data from such old version is no longer supported.
if (!storage) {
storage = CreateStorageObjectFromLegacyKeys();
}
DCHECK(storage);
for (NSString* key in dict) {
NSObject* object = dict[key];
if (object == [NSNull null]) {
[storage removeObjectForKey:key];
} else {
storage[key] = object;
}
}
[defaults setObject:storage forKey:kDefaultBrowserUtilsKey];
}
// Helper function to get the storage key for a specific promo type.
NSString* StorageKeyForDefaultPromoType(DefaultPromoType type) {
switch (type) {
case DefaultPromoTypeGeneral:
return kLastSignificantUserEventGeneral;
case DefaultPromoTypeMadeForIOS:
return kLastSignificantUserEventMadeForIOS;
case DefaultPromoTypeAllTabs:
return kLastSignificantUserEventAllTabs;
case DefaultPromoTypeStaySafe:
return kLastSignificantUserEventStaySafe;
}
NOTREACHED_IN_MIGRATION();
return nil;
}
// Loads from NSUserDefaults the time of the non-expired events for the
// given key into the given container.
void LoadActiveDatesForKey(NSString* key,
base::TimeDelta delay,
std::set<base::Time>& dates_set) {
NSArray* dates = GetObjectFromStorageForKey<NSArray>(key);
if (!dates) {
return;
}
const base::Time now = base::Time::Now();
for (NSObject* object : dates) {
NSDate* date = base::apple::ObjCCast<NSDate>(object);
if (!date) {
continue;
}
const base::Time time = base::Time::FromNSDate(date);
if (now - time > delay) {
continue;
}
dates_set.insert(time.LocalMidnight());
}
}
// Loads from NSUserDefaults the time of the non-expired events for the
// given key.
std::vector<base::Time> LoadActiveTimestampsForKey(NSString* key,
base::TimeDelta delay) {
NSArray* dates = GetObjectFromStorageForKey<NSArray>(key);
if (!dates) {
return {};
}
std::vector<base::Time> times;
times.reserve(dates.count);
const base::Time now = base::Time::Now();
for (NSObject* object : dates) {
NSDate* date = base::apple::ObjCCast<NSDate>(object);
if (!date) {
continue;
}
const base::Time time = base::Time::FromNSDate(date);
if (now - time > delay) {
continue;
}
times.push_back(time);
}
return times;
}
// Stores the time of the last recorded events for `key`.
void StoreTimestampsForKey(NSString* key, std::vector<base::Time> times) {
NSMutableArray<NSDate*>* dates =
[[NSMutableArray alloc] initWithCapacity:times.size()];
// Only record up to maxPastTimestampsToRecord timestamps.
size_t maxPastTimestampsToRecord = GetMaxPastTimestampsToRecord();
if (times.size() > maxPastTimestampsToRecord) {
const size_t count_to_erase = times.size() - maxPastTimestampsToRecord;
times.erase(times.begin(), times.begin() + count_to_erase);
}
for (base::Time time : times) {
[dates addObject:time.ToNSDate()];
}
SetObjectIntoStorageForKey(key, dates);
}
// Returns whether an event was logged for key occuring less than `delay`
// in the past.
bool HasRecordedEventForKeyLessThanDelay(NSString* key, base::TimeDelta delay) {
NSDate* date = GetObjectFromStorageForKey<NSDate>(key);
if (!date) {
return false;
}
const base::Time time = base::Time::FromNSDate(date);
return base::Time::Now() - time < delay;
}
// Returns whether an event was logged for key occuring more than `delay`
// in the past.
bool HasRecordedEventForKeyMoreThanDelay(NSString* key, base::TimeDelta delay) {
NSDate* date = GetObjectFromStorageForKey<NSDate>(key);
if (!date) {
return false;
}
const base::Time time = base::Time::FromNSDate(date);
return base::Time::Now() - time > delay;
}
// Returns true if there exists a recorded interaction with a non-modal promo
// more recent than the last recorded interaction with a fullscreen promo.
bool IsLastNonModalMoreRecentThanLastFullscreen() {
NSDate* last_non_modal_interaction = GetObjectFromStorageForKey<NSDate>(
kLastTimeUserInteractedWithNonModalPromo);
if (!last_non_modal_interaction) {
return false;
}
NSDate* last_fullscreen_interaction = GetObjectFromStorageForKey<NSDate>(
kLastTimeUserInteractedWithFullscreenPromo);
if (!last_fullscreen_interaction) {
return true;
}
NSComparisonResult comparison_result =
[last_non_modal_interaction compare:last_fullscreen_interaction];
return comparison_result == NSOrderedDescending;
}
// Copy the NSDate object in NSUserDefaults from the origin key to the
// destination key. Does nothing if the origin key is empty.
void CopyNSDateFromKeyToKey(NSString* originKey, NSString* destinationKey) {
NSDate* origin_date = GetObjectFromStorageForKey<NSDate>(originKey);
if (!origin_date) {
return;
}
SetObjectIntoStorageForKey(destinationKey, origin_date);
}
// Returns number of events logged for key occuring less than `delay` in the
// past.
int NumRecordedEventForKeyLessThanDelay(NSString* key, base::TimeDelta delay) {
return LoadActiveTimestampsForKey(key, delay).size();
}
// `YES` if user interacted with the first run default browser screen.
BOOL HasUserInteractedWithFirstRunPromoBefore() {
NSNumber* number =
GetObjectFromStorageForKey<NSNumber>(kUserHasInteractedWithFirstRunPromo);
return number.boolValue;
}
// Returns the number of time the fullscreen default browser promo has been
// displayed.
NSInteger GenericPromoInteractionCount() {
NSNumber* number =
GetObjectFromStorageForKey<NSNumber>(kGenericPromoInteractionCount);
return number.integerValue;
}
// Returns the number of time the tailored default browser promo has been
// displayed.
NSInteger TailoredPromoInteractionCount() {
NSNumber* number =
GetObjectFromStorageForKey<NSNumber>(kTailoredPromoInteractionCount);
return number.integerValue;
}
// Computes cooldown between fullscreen promos.
base::TimeDelta ComputeCooldown() {
// `true` if the user is in the short delay group experiment and tap on the
// "No thanks" button in first run default browser screen. Short cool down
// should be set only one time, so after the first run promo there is a short
// cool down before the next promo and after it goes back to normal.
if (DisplayedFullscreenPromoCount() < 2 &&
HasUserInteractedWithFirstRunPromoBefore()) {
return kPromosShortCoolDown;
}
return kFullscreenPromoCoolDown;
}
// Returns number of days since user last interacted with one of the promos.
int NumDaysSincePromoInteraction() {
NSDate* timestamp = GetObjectFromStorageForKey<NSDate>(
kLastTimeUserInteractedWithFullscreenPromo);
if (timestamp == nil) {
return 0;
}
int days = (base::Time::Now() - base::Time::FromNSDate(timestamp)).InDays();
if (days < 0) {
return 0;
}
return days;
}
// Returns number of days in past `kTriggerCriteriaExperimentStatExpiration`
// days when user opened chrome.
int NumActiveDays() {
std::set<base::Time> active_dates;
LoadActiveDatesForKey(kAllTimestampsAppLaunchColdStart,
kTriggerCriteriaExperimentStatExpiration, active_dates);
LoadActiveDatesForKey(kAllTimestampsAppLaunchWarmStart,
kTriggerCriteriaExperimentStatExpiration, active_dates);
LoadActiveDatesForKey(kAllTimestampsAppLaunchIndirectStart,
kTriggerCriteriaExperimentStatExpiration, active_dates);
return active_dates.size();
}
// Adds current timestamp in the array of timestamps for the given key.
void StoreCurrentTimestampForKey(NSString* key) {
std::vector<base::Time> timestamps =
LoadActiveTimestampsForKey(key, kTriggerCriteriaExperimentStatExpiration);
timestamps.push_back(base::Time::Now());
StoreTimestampsForKey(key, timestamps);
}
} // namespace
NSString* const kLastHTTPURLOpenTime = @"lastHTTPURLOpenTime";
NSString* const kLastTimeUserInteractedWithNonModalPromo =
@"lastTimeUserInteractedWithNonModalPromo";
NSString* const kLastTimeUserInteractedWithFullscreenPromo =
@"lastTimeUserInteractedWithFullscreenPromo";
NSString* const kAllTimestampsAppLaunchColdStart =
@"AllTimestampsAppLaunchColdStart";
NSString* const kAllTimestampsAppLaunchWarmStart =
@"AllTimestampsAppLaunchWarmStart";
NSString* const kAllTimestampsAppLaunchIndirectStart =
@"AllTimestampsAppLaunchIndirectStart";
NSString* const kLastSignificantUserEventStaySafe =
@"lastSignificantUserEventStaySafe";
NSString* const kOmniboxUseCount = @"OmniboxUseCount";
NSString* const kBookmarkUseCount = @"BookmarkUseCount";
NSString* const kAutofillUseCount = @"AutofillUseCount";
NSString* const kSpecialTabsUseCount = @"SpecialTabUseCount";
NSString* const kUserHasInteractedWithFullscreenPromo =
@"userHasInteractedWithFullscreenPromo";
NSString* const kUserHasInteractedWithTailoredFullscreenPromo =
@"userHasInteractedWithTailoredFullscreenPromo";
NSString* const kUserHasInteractedWithFirstRunPromo =
@"userHasInteractedWithFirstRunPromo";
NSString* const kDisplayedFullscreenPromoCount = @"displayedPromoCount";
NSString* const kGenericPromoInteractionCount = @"genericPromoInteractionCount";
NSString* const kTailoredPromoInteractionCount =
@"tailoredPromoInteractionCount";
constexpr base::TimeDelta kBlueDotPromoDuration = base::Days(15);
constexpr base::TimeDelta kBlueDotPromoReoccurrancePeriod = base::Days(360);
// Migration to FET keys.
NSString* const kFRETimestampMigrationDone = @"fre_timestamp_migration_done";
NSString* const kPromoInterestEventMigrationDone =
@"promo_interest_event_migration_done";
NSString* const kPromoImpressionsMigrationDone =
@"promo_impressions_migration_done";
NSString* const kTimestampTriggerCriteriaExperimentStarted =
@"TimestampTriggerCriteriaExperimentStarted";
std::vector<base::Time> LoadTimestampsForPromoType(DefaultPromoType type) {
return LoadActiveTimestampsForKey(StorageKeyForDefaultPromoType(type),
kUserActivityTimestampExpiration);
}
void StoreTimestampsForPromoType(DefaultPromoType type,
std::vector<base::Time> times) {
StoreTimestampsForKey(StorageKeyForDefaultPromoType(type), times);
}
void SetObjectIntoStorageForKey(NSString* key, NSObject* data) {
UpdateStorageWithDictionary(@{key : data});
}
void LogOpenHTTPURLFromExternalURL() {
SetObjectIntoStorageForKey(kLastHTTPURLOpenTime, [NSDate date]);
}
void LogLikelyInterestedDefaultBrowserUserActivity(DefaultPromoType type) {
std::vector<base::Time> times = LoadTimestampsForPromoType(type);
times.push_back(base::Time::Now());
StoreTimestampsForPromoType(type, std::move(times));
}
void LogToFETDefaultBrowserPromoShown(feature_engagement::Tracker* tracker) {
// OTR browsers can sometimes pass a null tracker, check for that here.
if (!tracker) {
return;
}
tracker->NotifyEvent(feature_engagement::events::kDefaultBrowserPromoShown);
}
bool HasDefaultBrowserBlueDotDisplayTimestamp() {
return !GetApplicationContext()
->GetLocalState()
->FindPreference(
prefs::kIosDefaultBrowserBlueDotPromoFirstDisplay)
->IsDefaultValue();
}
void ResetDefaultBrowserBlueDotDisplayTimestampIfNeeded() {
BOOL has_timestamp = HasDefaultBrowserBlueDotDisplayTimestamp();
if (!has_timestamp) {
return;
}
base::Time timestamp = GetApplicationContext()->GetLocalState()->GetTime(
prefs::kIosDefaultBrowserBlueDotPromoFirstDisplay);
// If more than `kBlueDotPromoReoccurrancePeriod` past since previous blue
// dot display, user should again become eligible for blue dot promo.
if (base::Time::Now() - timestamp >= kBlueDotPromoReoccurrancePeriod) {
GetApplicationContext()->GetLocalState()->ClearPref(
prefs::kIosDefaultBrowserBlueDotPromoFirstDisplay);
}
}
void RecordDefaultBrowserBlueDotFirstDisplay() {
if (!HasDefaultBrowserBlueDotDisplayTimestamp()) {
GetApplicationContext()->GetLocalState()->SetTime(
prefs::kIosDefaultBrowserBlueDotPromoFirstDisplay, base::Time::Now());
}
}
bool ShouldTriggerDefaultBrowserHighlightFeature(
feature_engagement::Tracker* tracker) {
if (IsChromeLikelyDefaultBrowser()) {
return false;
}
ResetDefaultBrowserBlueDotDisplayTimestampIfNeeded();
if (HasDefaultBrowserBlueDotDisplayTimestamp()) {
base::Time timestamp = GetApplicationContext()->GetLocalState()->GetTime(
prefs::kIosDefaultBrowserBlueDotPromoFirstDisplay);
if (base::Time::Now() - timestamp >= kBlueDotPromoDuration) {
return false;
}
}
// We ask the appropriate FET feature if it should trigger, i.e. if we
// should show the blue dot promo badge.
if (tracker->ShouldTriggerHelpUI(
feature_engagement::kIPHiOSDefaultBrowserOverflowMenuBadgeFeature)) {
tracker->Dismissed(
feature_engagement::kIPHiOSDefaultBrowserOverflowMenuBadgeFeature);
return true;
}
return false;
}
bool IsDefaultBrowserTriggerCriteraExperimentEnabled() {
return base::FeatureList::IsEnabled(
feature_engagement::kDefaultBrowserTriggerCriteriaExperiment);
}
void SetTriggerCriteriaExperimentStartTimestamp() {
SetObjectIntoStorageForKey(kTimestampTriggerCriteriaExperimentStarted,
[NSDate date]);
}
bool HasTriggerCriteriaExperimentStarted() {
NSDate* date = GetObjectFromStorageForKey<NSDate>(
kTimestampTriggerCriteriaExperimentStarted);
return date != nil;
}
bool HasTriggerCriteriaExperimentStarted21days() {
return HasRecordedEventForKeyMoreThanDelay(
kTimestampTriggerCriteriaExperimentStarted, base::Days(21));
}
bool IsNonModalDefaultBrowserPromoCooldownRefactorEnabled() {
return base::FeatureList::IsEnabled(
kNonModalDefaultBrowserPromoCooldownRefactor);
}
bool HasUserInteractedWithFullscreenPromoBefore() {
if (base::FeatureList::IsEnabled(
feature_engagement::kDefaultBrowserEligibilitySlidingWindow)) {
// When the total promo count is 1 it means that user has seen only the FRE
// promo. The cooldown from FRE will be taken care of in
// ```ComputeCooldown```. Here we only need to check the timestamp of the
// last promo if users seen more than FRE.
return DisplayedFullscreenPromoCount() > 1 &&
HasRecordedEventForKeyLessThanDelay(
kLastTimeUserInteractedWithFullscreenPromo,
base::Days(
feature_engagement::
kDefaultBrowserEligibilitySlidingWindowParam.Get()));
}
NSNumber* number = GetObjectFromStorageForKey<NSNumber>(
kUserHasInteractedWithFullscreenPromo);
return number.boolValue;
}
bool HasUserInteractedWithTailoredFullscreenPromoBefore() {
NSNumber* number = GetObjectFromStorageForKey<NSNumber>(
kUserHasInteractedWithTailoredFullscreenPromo);
return number.boolValue;
}
NSInteger UserInteractionWithNonModalPromoCount() {
NSNumber* number = GetObjectFromStorageForKey<NSNumber>(
kUserInteractedWithNonModalPromoCount);
return number.integerValue;
}
NSInteger DisplayedFullscreenPromoCount() {
NSNumber* number =
GetObjectFromStorageForKey<NSNumber>(kDisplayedFullscreenPromoCount);
return number.integerValue;
}
void LogFullscreenDefaultBrowserPromoDisplayed() {
const NSInteger displayed_promo_count = DisplayedFullscreenPromoCount();
NSDictionary<NSString*, NSObject*>* update = @{
kDisplayedFullscreenPromoCount : @(displayed_promo_count + 1),
};
UpdateStorageWithDictionary(update);
}
void LogUserInteractionWithFullscreenPromo() {
const NSInteger generic_promo_interaction_count =
GenericPromoInteractionCount();
NSDictionary<NSString*, NSObject*>* update = @{
kUserHasInteractedWithFullscreenPromo : @YES,
kLastTimeUserInteractedWithFullscreenPromo : [NSDate date],
kGenericPromoInteractionCount : @(generic_promo_interaction_count + 1),
};
UpdateStorageWithDictionary(update);
}
void LogUserInteractionWithTailoredFullscreenPromo() {
const NSInteger tailored_promo_interaction_count =
TailoredPromoInteractionCount();
UpdateStorageWithDictionary(@{
kUserHasInteractedWithTailoredFullscreenPromo : @YES,
kLastTimeUserInteractedWithFullscreenPromo : [NSDate date],
kTailoredPromoInteractionCount : @(tailored_promo_interaction_count + 1),
});
}
void LogUserInteractionWithNonModalPromo(
NSInteger currentNonModalPromoInteractionsCount,
NSInteger currentFullscreenPromoInteractionsCount) {
if (IsNonModalDefaultBrowserPromoCooldownRefactorEnabled()) {
UpdateStorageWithDictionary(@{
kLastTimeUserInteractedWithNonModalPromo : [NSDate date],
kUserInteractedWithNonModalPromoCount :
@(currentNonModalPromoInteractionsCount + 1),
});
} else {
UpdateStorageWithDictionary(@{
kLastTimeUserInteractedWithFullscreenPromo : [NSDate date],
kUserInteractedWithNonModalPromoCount :
@(currentNonModalPromoInteractionsCount + 1),
kDisplayedFullscreenPromoCount :
@(currentFullscreenPromoInteractionsCount + 1),
});
}
}
void LogUserInteractionWithFirstRunPromo() {
const NSInteger displayed_promo_count = DisplayedFullscreenPromoCount();
UpdateStorageWithDictionary(@{
kUserHasInteractedWithFirstRunPromo : @YES,
kLastTimeUserInteractedWithFullscreenPromo : [NSDate date],
kDisplayedFullscreenPromoCount : @(displayed_promo_count + 1),
});
}
void CleanupStorageForTriggerExperiment() {
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
[defaults removeObjectForKey:kAllTimestampsAppLaunchColdStart];
[defaults removeObjectForKey:kAllTimestampsAppLaunchWarmStart];
[defaults removeObjectForKey:kAllTimestampsAppLaunchIndirectStart];
[defaults removeObjectForKey:kAutofillUseCount];
[defaults removeObjectForKey:kSpecialTabsUseCount];
[defaults removeObjectForKey:kOmniboxUseCount];
}
void LogCopyPasteInOmniboxForCriteriaExperiment() {
if (!IsDefaultBrowserTriggerCriteraExperimentEnabled()) {
CleanupStorageForTriggerExperiment();
return;
}
StoreCurrentTimestampForKey(kOmniboxUseCount);
}
void LogBookmarkUseForCriteriaExperiment() {
if (!IsDefaultBrowserTriggerCriteraExperimentEnabled()) {
CleanupStorageForTriggerExperiment();
return;
}
StoreCurrentTimestampForKey(kBookmarkUseCount);
}
void LogAutofillUseForCriteriaExperiment() {
if (!IsDefaultBrowserTriggerCriteraExperimentEnabled()) {
CleanupStorageForTriggerExperiment();
return;
}
StoreCurrentTimestampForKey(kAutofillUseCount);
}
void LogRemoteTabsUseForCriteriaExperiment() {
if (!IsDefaultBrowserTriggerCriteraExperimentEnabled()) {
CleanupStorageForTriggerExperiment();
return;
}
StoreCurrentTimestampForKey(kSpecialTabsUseCount);
}
bool IsChromeLikelyDefaultBrowserXDays(int days) {
return HasRecordedEventForKeyLessThanDelay(kLastHTTPURLOpenTime,
base::Days(days));
}
bool IsChromeLikelyDefaultBrowser() {
return HasRecordedEventForKeyLessThanDelay(kLastHTTPURLOpenTime,
kLatestURLOpenForDefaultBrowser);
}
bool IsChromeLikelyDefaultBrowser7Days() {
return HasRecordedEventForKeyLessThanDelay(kLastHTTPURLOpenTime,
base::Days(7));
}
bool IsChromePotentiallyNoLongerDefaultBrowser(int likelyDefaultInterval,
int likelyNotDefaultInterval) {
bool wasLikelyDefaultBrowser =
IsChromeLikelyDefaultBrowserXDays(likelyDefaultInterval);
bool isStillLikelyDefaultBrowser =
IsChromeLikelyDefaultBrowserXDays(likelyNotDefaultInterval);
return wasLikelyDefaultBrowser && !isStillLikelyDefaultBrowser;
}
bool IsLikelyInterestedDefaultBrowserUser(DefaultPromoType promo_type) {
std::vector<base::Time> times = LoadTimestampsForPromoType(promo_type);
return !times.empty();
}
bool UserInFullscreenPromoCooldown() {
// Sets the last fullscreen promo interaction to the same value as the last
// non-modal promo interaction if the latter is more recent. This is
// to allow a smooth transition back from the cooldown period separation
// between the two promo types, if a rollback is needed.
if (!IsNonModalDefaultBrowserPromoCooldownRefactorEnabled() &&
IsLastNonModalMoreRecentThanLastFullscreen()) {
CopyNSDateFromKeyToKey(kLastTimeUserInteractedWithNonModalPromo,
kLastTimeUserInteractedWithFullscreenPromo);
}
return HasRecordedEventForKeyLessThanDelay(
kLastTimeUserInteractedWithFullscreenPromo, ComputeCooldown());
}
bool UserInNonModalPromoCooldown() {
NSDate* last_interaction = GetObjectFromStorageForKey<NSDate>(
kLastTimeUserInteractedWithNonModalPromo);
// Sets the last non-modal promo interaction to the same value as last
// fullscreen promo interaction if no non-modal interaction is found. This is
// to allow a smooth transition to the cooldown period separation between the
// two promo types.
if (!last_interaction) {
CopyNSDateFromKeyToKey(kLastTimeUserInteractedWithFullscreenPromo,
kLastTimeUserInteractedWithNonModalPromo);
}
return HasRecordedEventForKeyLessThanDelay(
kLastTimeUserInteractedWithNonModalPromo,
base::Days(kNonModalDefaultBrowserPromoCooldownRefactorParam.Get()));
}
// Visible for testing.
NSString* const kDefaultBrowserUtilsKey = @"DefaultBrowserUtils";
// Visible for testing.
const NSArray<NSString*>* DefaultBrowserUtilsLegacyKeysForTesting() {
NSArray<NSString*>* const keysForTesting = @[
// clang-format off
kLastHTTPURLOpenTime,
kLastSignificantUserEventGeneral,
kLastSignificantUserEventStaySafe,
kLastSignificantUserEventMadeForIOS,
kLastSignificantUserEventAllTabs,
kLastTimeUserInteractedWithFullscreenPromo,
kLastTimeUserInteractedWithNonModalPromo,
kUserHasInteractedWithFullscreenPromo,
kUserHasInteractedWithTailoredFullscreenPromo,
kUserHasInteractedWithFirstRunPromo,
kUserInteractedWithNonModalPromoCount,
kDisplayedFullscreenPromoCount,
kTailoredPromoInteractionCount,
kGenericPromoInteractionCount,
// clang-format on
];
return keysForTesting;
}
int GetNonModalDefaultBrowserPromoImpressionLimit() {
int limit = kNonModalDefaultBrowserPromoImpressionLimitParam.Get();
// The histogram only supports up to 10 impressions.
if (limit > 10) {
limit = 10;
}
return limit;
}
bool IsPostRestoreDefaultBrowserEligibleUser() {
return IsFirstSessionAfterDeviceRestore() == signin::Tribool::kTrue &&
IsChromeLikelyDefaultBrowser();
}
DefaultPromoTypeForUMA GetDefaultPromoTypeForUMA(DefaultPromoType type) {
switch (type) {
case DefaultPromoTypeGeneral:
return DefaultPromoTypeForUMA::kGeneral;
case DefaultPromoTypeMadeForIOS:
return DefaultPromoTypeForUMA::kMadeForIOS;
case DefaultPromoTypeStaySafe:
return DefaultPromoTypeForUMA::kStaySafe;
case DefaultPromoTypeAllTabs:
return DefaultPromoTypeForUMA::kAllTabs;
default:
NOTREACHED();
}
}
void LogDefaultBrowserPromoHistogramForAction(
DefaultPromoType type,
IOSDefaultBrowserPromoAction action) {
switch (type) {
case DefaultPromoTypeGeneral:
base::UmaHistogramEnumeration("IOS.DefaultBrowserFullscreenPromo",
action);
break;
case DefaultPromoTypeAllTabs:
base::UmaHistogramEnumeration(
"IOS.DefaultBrowserFullscreenTailoredPromoAllTabs", action);
break;
case DefaultPromoTypeMadeForIOS:
base::UmaHistogramEnumeration(
"IOS.DefaultBrowserFullscreenTailoredPromoMadeForIOS", action);
break;
case DefaultPromoTypeStaySafe:
base::UmaHistogramEnumeration(
"IOS.DefaultBrowserFullscreenTailoredPromoStaySafe", action);
break;
default:
NOTREACHED();
}
}
const std::string IOSDefaultBrowserPromoActionToString(
IOSDefaultBrowserPromoAction action) {
switch (action) {
case IOSDefaultBrowserPromoAction::kActionButton:
return "PrimaryAction";
case IOSDefaultBrowserPromoAction::kCancel:
return "Cancel";
case IOSDefaultBrowserPromoAction::kDismiss:
return "Dismiss";
case IOSDefaultBrowserPromoAction::kRemindMeLater:
default:
NOTREACHED();
}
}
void RecordPromoStatsToUMAForActionString(PromoStatistics* promo_stats,
const std::string& action_str) {
if (!IsDefaultBrowserTriggerCriteraExperimentEnabled()) {
return;
}
std::string histogram_prefix =
base::StrCat({"IOS.DefaultBrowserPromo.", action_str});
base::UmaHistogramCounts100(
base::StrCat({histogram_prefix, ".PromoDisplayCount"}),
promo_stats.promoDisplayCount);
base::UmaHistogramCounts1000(
base::StrCat({histogram_prefix, ".LastPromoInteractionNumDays"}),
promo_stats.numDaysSinceLastPromo);
base::UmaHistogramCounts1000(
base::StrCat({histogram_prefix, ".ChromeColdStartCount"}),
promo_stats.chromeColdStartCount);
base::UmaHistogramCounts1000(
base::StrCat({histogram_prefix, ".ChromeWarmStartCount"}),
promo_stats.chromeWarmStartCount);
base::UmaHistogramCounts100(
base::StrCat({histogram_prefix, ".ChromeIndirectStartCount"}),
promo_stats.chromeIndirectStartCount);
base::UmaHistogramCounts100(
base::StrCat({histogram_prefix, ".PasswordManagerUseCount"}),
promo_stats.passwordManagerUseCount);
base::UmaHistogramCounts100(
base::StrCat({histogram_prefix, ".OmniboxClipboardUseCount"}),
promo_stats.omniboxClipboardUseCount);
base::UmaHistogramCounts100(
base::StrCat({histogram_prefix, ".BookmarkUseCount"}),
promo_stats.bookmarkUseCount);
base::UmaHistogramCounts100(
base::StrCat({histogram_prefix, ".AutofllUseCount"}),
promo_stats.autofillUseCount);
base::UmaHistogramCounts100(
base::StrCat({histogram_prefix, ".SpecialTabsUseCount"}),
promo_stats.specialTabsUseCount);
}
PromoStatistics* CalculatePromoStatistics() {
if (!IsDefaultBrowserTriggerCriteraExperimentEnabled()) {
return nil;
}
PromoStatistics* promo_stats = [[PromoStatistics alloc] init];
promo_stats.promoDisplayCount = DisplayedFullscreenPromoCount();
promo_stats.numDaysSinceLastPromo = NumDaysSincePromoInteraction();
promo_stats.chromeColdStartCount = NumRecordedEventForKeyLessThanDelay(
kAllTimestampsAppLaunchColdStart,
kTriggerCriteriaExperimentStatExpiration);
promo_stats.chromeWarmStartCount = NumRecordedEventForKeyLessThanDelay(
kAllTimestampsAppLaunchWarmStart,
kTriggerCriteriaExperimentStatExpiration);
promo_stats.chromeIndirectStartCount = NumRecordedEventForKeyLessThanDelay(
kAllTimestampsAppLaunchIndirectStart,
kTriggerCriteriaExperimentStatExpiration);
promo_stats.activeDayCount = NumActiveDays();
promo_stats.passwordManagerUseCount = NumRecordedEventForKeyLessThanDelay(
kLastSignificantUserEventStaySafe,
kTriggerCriteriaExperimentStatExpiration);
promo_stats.omniboxClipboardUseCount = NumRecordedEventForKeyLessThanDelay(
kOmniboxUseCount, kTriggerCriteriaExperimentStatExpiration);
promo_stats.bookmarkUseCount = NumRecordedEventForKeyLessThanDelay(
kBookmarkUseCount, kTriggerCriteriaExperimentStatExpiration);
promo_stats.autofillUseCount = NumRecordedEventForKeyLessThanDelay(
kAutofillUseCount, kTriggerCriteriaExperimentStatExpiration);
promo_stats.specialTabsUseCount = NumRecordedEventForKeyLessThanDelay(
kSpecialTabsUseCount, kTriggerCriteriaExperimentStatExpiration);
return promo_stats;
}
void RecordPromoStatsToUMAForAction(PromoStatistics* promo_stats,
IOSDefaultBrowserPromoAction action) {
RecordPromoStatsToUMAForActionString(
promo_stats, IOSDefaultBrowserPromoActionToString(action));
}
void RecordPromoStatsToUMAForAppear(PromoStatistics* promo_stats) {
RecordPromoStatsToUMAForActionString(promo_stats, kAppearAction);
}
void RecordPromoDisplayStatsToUMA() {
base::UmaHistogramCounts1000(
"IOS.DefaultBrowserPromo.DaysSinceLastPromoInteraction",
NumDaysSincePromoInteraction());
base::UmaHistogramCounts100(
"IOS.DefaultBrowserPromo.GenericPromoDisplayCount",
GenericPromoInteractionCount());
base::UmaHistogramCounts100(
"IOS.DefaultBrowserPromo.TailoredPromoDisplayCount",
TailoredPromoInteractionCount());
}
void LogBrowserLaunched(bool is_cold_start) {
if (!IsDefaultBrowserTriggerCriteraExperimentEnabled()) {
CleanupStorageForTriggerExperiment();
return;
}
NSString* key = is_cold_start ? kAllTimestampsAppLaunchColdStart
: kAllTimestampsAppLaunchWarmStart;
StoreCurrentTimestampForKey(key);
}
void LogBrowserIndirectlylaunched() {
if (!IsDefaultBrowserTriggerCriteraExperimentEnabled()) {
CleanupStorageForTriggerExperiment();
return;
}
StoreCurrentTimestampForKey(kAllTimestampsAppLaunchIndirectStart);
}
// Migration to FET
base::Time GetDefaultBrowserFREPromoTimestampIfLast() {
// Get FRE promo timestamp. It is the last seen timestamp if user has seen
// only 1 promo. If user has seen more promos, then we assume that FRE
// happened far past enough for it not be important.
if (HasUserInteractedWithFirstRunPromoBefore() &&
DisplayedFullscreenPromoCount() == 1) {
NSDate* timestamp = GetObjectFromStorageForKey<NSDate>(
kLastTimeUserInteractedWithFullscreenPromo);
if (timestamp != nil) {
return base::Time::FromNSDate(timestamp);
}
}
return base::Time::UnixEpoch();
}
base::Time GetGenericDefaultBrowserPromoTimestamp() {
// Get the latest promo timestamp if user has seen the generic promo before
// even when the generic promo is not the latest promo. This is the best we
// can get considering the actual timestamp is overwritten.
NSNumber* number = GetObjectFromStorageForKey<NSNumber>(
kUserHasInteractedWithFullscreenPromo);
if (number.boolValue) {
NSDate* timestamp = GetObjectFromStorageForKey<NSDate>(
kLastTimeUserInteractedWithFullscreenPromo);
if (timestamp != nil) {
return base::Time::FromNSDate(timestamp);
}
}
return base::Time::UnixEpoch();
}
base::Time GetTailoredDefaultBrowserPromoTimestamp() {
// Get the latest promo timestamp if user has seen the tailored promo before
// even when the tailored promo is not the latest promo. This is the best we
// can get considering the actual timestamp is overwritten.
if (HasUserInteractedWithTailoredFullscreenPromoBefore()) {
NSDate* timestamp = GetObjectFromStorageForKey<NSDate>(
kLastTimeUserInteractedWithFullscreenPromo);
if (timestamp != nil) {
return base::Time::FromNSDate(timestamp);
}
}
return base::Time::UnixEpoch();
}
void LogFRETimestampMigrationDone() {
NSDictionary<NSString*, NSObject*>* update =
@{kFRETimestampMigrationDone : @YES};
UpdateStorageWithDictionary(update);
}
BOOL FRETimestampMigrationDone() {
NSNumber* number =
GetObjectFromStorageForKey<NSNumber>(kFRETimestampMigrationDone);
return number.boolValue;
}
void LogPromoInterestEventMigrationDone() {
NSDictionary<NSString*, NSObject*>* update =
@{kPromoInterestEventMigrationDone : @YES};
UpdateStorageWithDictionary(update);
}
BOOL IsPromoInterestEventMigrationDone() {
NSNumber* number =
GetObjectFromStorageForKey<NSNumber>(kPromoInterestEventMigrationDone);
return number.boolValue;
}
void LogPromoImpressionsMigrationDone() {
NSDictionary<NSString*, NSObject*>* update =
@{kPromoImpressionsMigrationDone : @YES};
UpdateStorageWithDictionary(update);
}
BOOL IsPromoImpressionsMigrationDone() {
NSNumber* number =
GetObjectFromStorageForKey<NSNumber>(kPromoImpressionsMigrationDone);
return number.boolValue;
}
void RecordDefaultBrowserPromoLastAction(IOSDefaultBrowserPromoAction action) {
GetApplicationContext()->GetLocalState()->SetInteger(
prefs::kIosDefaultBrowserPromoLastAction, static_cast<int>(action));
}
std::optional<IOSDefaultBrowserPromoAction> DefaultBrowserPromoLastAction() {
const PrefService::Preference* last_action =
GetApplicationContext()->GetLocalState()->FindPreference(
prefs::kIosDefaultBrowserPromoLastAction);
if (last_action->IsDefaultValue()) {
return std::nullopt;
}
int last_action_int = last_action->GetValue()->GetInt();
return static_cast<IOSDefaultBrowserPromoAction>(last_action_int);
}