| // 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"; |
| |
| // 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); |
| |
| // Cool down between non-modal promos. |
| constexpr base::TimeDelta kNonModalPromoCoolDown = 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(); |
| } |
| |
| // 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; |
| } |
| |
| // 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 kUserInteractedWithNonModalPromoCount = |
| @"userInteractedWithNonModalPromoCount"; |
| 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"; |
| NSString* const kNonModalPromoMigrationDone = @"kNonModalPromoMigrationDone"; |
| |
| 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 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) { |
| UpdateStorageWithDictionary(@{ |
| kLastTimeUserInteractedWithNonModalPromo : [NSDate date], |
| kUserInteractedWithNonModalPromoCount : |
| @(currentNonModalPromoInteractionsCount + 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() { |
| 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, kNonModalPromoCoolDown); |
| } |
| |
| // 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, ".ActiveDayCount"}), |
| promo_stats.activeDayCount); |
| 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 LogNonModalPromoMigrationDone() { |
| NSDictionary<NSString*, NSObject*>* update = |
| @{kNonModalPromoMigrationDone : @YES}; |
| UpdateStorageWithDictionary(update); |
| } |
| |
| bool IsNonModalPromoMigrationDone() { |
| NSNumber* number = |
| GetObjectFromStorageForKey<NSNumber>(kNonModalPromoMigrationDone); |
| 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); |
| } |
| |
| NSDate* LastTimeUserInteractedWithNonModalPromo() { |
| return GetObjectFromStorageForKey<NSDate>( |
| kLastTimeUserInteractedWithNonModalPromo); |
| } |