blob: 1a24670548682c38104bddeb83654fce7e3fc49d [file] [log] [blame]
// 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/utils.h"
#import "base/ios/ios_util.h"
#import "base/mac/foundation_util.h"
#import "base/metrics/field_trial_params.h"
#import "base/metrics/user_metrics.h"
#import "base/notreached.h"
#import "base/time/time.h"
#import "components/feature_engagement/public/event_constants.h"
#import "components/feature_engagement/public/feature_constants.h"
#import "components/feature_engagement/public/tracker.h"
#import "components/sync/driver/sync_service.h"
#import "ios/chrome/browser/feature_engagement/tracker_factory.h"
#import "ios/chrome/browser/settings/sync/utils/identity_error_util.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
#import <UIKit/UIKit.h>
// Key in NSUserDefaults containing an NSDictionary used to store all the
// information.
extern NSString* const kDefaultBrowserUtilsKey;
namespace {
// Key in storage containing an NSDate corresponding to the last time
// an HTTP(S) link was sent and opened by the app.
NSString* const kLastHTTPURLOpenTime = @"lastHTTPURLOpenTime";
// 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 stay safe event of interest for Default Browser Promo modals.
NSString* const kLastSignificantUserEventStaySafe =
@"lastSignificantUserEventStaySafe";
// 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 NSDate indicating the last time a user
// interacted with ANY promo. The string value is kept from when the promos
// first launched to avoid changing the behavior for users that have already
// seen the promo.
NSString* const kLastTimeUserInteractedWithPromo =
@"lastTimeUserInteractedWithFullscreenPromo";
// Key in storage containing a bool indicating if the user has
// previously interacted with a regular fullscreen promo.
NSString* const kUserHasInteractedWithFullscreenPromo =
@"userHasInteractedWithFullscreenPromo";
// Key in storage containing a bool indicating if the user has
// previously interacted with a tailored fullscreen promo.
NSString* const kUserHasInteractedWithTailoredFullscreenPromo =
@"userHasInteractedWithTailoredFullscreenPromo";
// Key in storage containing a bool indicating if the user has
// previously interacted with first run promo.
NSString* const kUserHasInteractedWithFirstRunPromo =
@"userHasInteractedWithFirstRunPromo";
// Key in storage containing an int indicating the number of times the
// user has interacted with a non-modal promo.
NSString* const kUserInteractedWithNonModalPromoCount =
@"userInteractedWithNonModalPromoCount";
// Key in storage containing an int indicating the number of times a
// promo has been displayed.
NSString* const kDisplayedPromoCount = @"displayedPromoCount";
// Key in storage containing an NSDate indicating the last time a user
// interacted with the "remind me later" panel.
NSString* const kRemindMeLaterPromoActionInteraction =
@"remindMeLaterPromoActionInteraction";
// Key in storage containing a bool indicating if the user tapped on
// button to open settings.
NSString* const kOpenSettingsActionInteraction =
@"openSettingsActionInteraction";
// Key in storage containing the timestamp of the last time the user opened the
// app via first-party intent.
NSString* const kTimestampAppLastOpenedViaFirstPartyIntent =
@"TimestampAppLastOpenedViaFirstPartyIntent";
// Key in storage containing the timestamp of the last time the user pasted a
// valid URL into the omnibox.
NSString* const kTimestampLastValidURLPasted = @"TimestampLastValidURLPasted";
const char kDefaultBrowserFullscreenPromoExperimentChangeStringsGroupParam[] =
"show_switch_description";
// Maximum number of past event timestamps to record.
const size_t kMaxPastTimestampsToRecord = 10;
// 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);
// Delay for the user to be reshown the fullscreen promo when the user taps on
// the "Remind Me Later" button.
constexpr base::TimeDelta kRemindMeLaterPresentationDelay = base::Hours(50);
// Cool down between fullscreen promos.
constexpr base::TimeDelta kFullscreenPromoCoolDown = base::Days(14);
// Short cool down between promos.
constexpr base::TimeDelta kPromosShortCoolDown = base::Days(3);
// Maximum time range between first-party app launches to notify the FET.
constexpr base::TimeDelta kMaximumTimeBetweenFirstPartyAppLaunches =
base::Days(7);
// Maximum time representing one user session.
constexpr base::TimeDelta kMaximumTimeOneUserSession = base::Hours(6);
// Maximum time range between valid user URL pastes to notify the FET.
constexpr base::TimeDelta kMaximumTimeBetweenValidURLPastes = base::Days(7);
// List of DefaultPromoType considered by MostRecentInterestDefaultPromoType.
const DefaultPromoType kDefaultPromoTypes[] = {
DefaultPromoTypeStaySafe,
DefaultPromoTypeAllTabs,
DefaultPromoTypeMadeForIOS,
};
// 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::mac::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 set `data` for `key` into the storage object.
void SetObjectIntoStorageForKey(NSString* key, NSObject* data) {
UpdateStorageWithDictionary(@{key : data});
}
// 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();
return nil;
}
// Loads from NSUserDefaults the time of the last non-expired events.
std::vector<base::Time> LoadTimestampsForPromoType(DefaultPromoType type) {
NSString* key = StorageKeyForDefaultPromoType(type);
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::mac::ObjCCast<NSDate>(object);
if (!date) {
continue;
}
const base::Time time = base::Time::FromNSDate(date);
if (now - time > kUserActivityTimestampExpiration) {
continue;
}
times.push_back(time);
}
return times;
}
// Stores the time of the last recorded events for `type`.
void StoreTimestampsForPromoType(DefaultPromoType type,
std::vector<base::Time> times) {
NSMutableArray<NSDate*>* dates =
[[NSMutableArray alloc] initWithCapacity:times.size()];
// Only record up to kMaxPastTimestampsToRecord timestamps.
if (times.size() > kMaxPastTimestampsToRecord) {
const size_t count_to_erase = times.size() - kMaxPastTimestampsToRecord;
times.erase(times.begin(), times.begin() + count_to_erase);
}
for (base::Time time : times) {
[dates addObject:time.ToNSDate()];
}
NSString* key = StorageKeyForDefaultPromoType(type);
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;
}
// `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 a default browser promo has been displayed.
NSInteger DisplayedPromoCount() {
NSNumber* number = GetObjectFromStorageForKey<NSNumber>(kDisplayedPromoCount);
return number.integerValue;
}
// Computes cool down between 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 (DisplayedPromoCount() < 2 && HasUserInteractedWithFirstRunPromoBefore() &&
!HasUserOpenedSettingsFromFirstRunPromo()) {
return kPromosShortCoolDown;
}
return kFullscreenPromoCoolDown;
}
} // namespace
const char kDefaultBrowserFullscreenPromoExperimentRemindMeGroupParam[] =
"show_remind_me_later";
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 LogRemindMeLaterPromoActionInteraction() {
DCHECK(IsInRemindMeLaterGroup());
SetObjectIntoStorageForKey(kRemindMeLaterPromoActionInteraction,
[NSDate date]);
}
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);
}
void LogToFETUserPastedURLIntoOmnibox(feature_engagement::Tracker* tracker) {
base::RecordAction(
base::UserMetricsAction("Mobile.Omnibox.iOS.PastedValidURL"));
// OTR browsers can sometimes pass a null tracker, check for that here.
if (!tracker) {
return;
}
if (HasRecentValidURLPastesAndRecordsCurrentPaste()) {
tracker->NotifyEvent(feature_engagement::events::kBlueDotPromoCriterionMet);
}
}
bool ShouldShowRemindMeLaterDefaultBrowserFullscreenPromo() {
if (!IsInRemindMeLaterGroup()) {
return false;
}
return HasRecordedEventForKeyMoreThanDelay(
kRemindMeLaterPromoActionInteraction, kRemindMeLaterPresentationDelay);
}
bool ShouldTriggerDefaultBrowserHighlightFeature(
const base::Feature& feature,
feature_engagement::Tracker* tracker,
syncer::SyncService* syncService) {
// TODO(crbug.com/1410229) clean-up experiment code when fully launched.
if (!IsBlueDotPromoEnabled() || IsChromeLikelyDefaultBrowser() ||
(syncService && ShouldIndicateIdentityErrorInOverflowMenu(syncService))) {
return false;
}
// We need to ask the FET whether or not we should show this IPH because if
// yes, this will automatically notify the other dependent FET features that
// their criteria have been met. We then automatically dismiss it. Since it's
// just a shadow feature to enable the other two needed for the blue dot
// promo, we ignore `ShouldTriggerHelpUI`'s return value.
if (tracker->ShouldTriggerHelpUI(
feature_engagement::kIPHiOSDefaultBrowserBadgeEligibilityFeature)) {
tracker->Dismissed(
feature_engagement::kIPHiOSDefaultBrowserBadgeEligibilityFeature);
}
// Now, 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)) {
tracker->Dismissed(feature);
return true;
}
return false;
}
bool IsInRemindMeLaterGroup() {
std::string paramValue = base::GetFieldTrialParamValueByFeature(
kDefaultBrowserFullscreenPromoExperiment,
kDefaultBrowserFullscreenPromoExperimentRemindMeGroupParam);
return !paramValue.empty();
}
bool IsInModifiedStringsGroup() {
std::string paramValue = base::GetFieldTrialParamValueByFeature(
kDefaultBrowserFullscreenPromoExperiment,
kDefaultBrowserFullscreenPromoExperimentChangeStringsGroupParam);
return !paramValue.empty();
}
bool AreDefaultBrowserPromosEnabled() {
if (base::FeatureList::IsEnabled(kDefaultBrowserBlueDotPromo)) {
return kBlueDotPromoUserGroupParam.Get() ==
BlueDotPromoUserGroup::kAllDBPromosEnabled;
}
return true;
}
bool IsBlueDotPromoEnabled() {
if (base::FeatureList::IsEnabled(kDefaultBrowserBlueDotPromo)) {
return kBlueDotPromoUserGroupParam.Get() ==
BlueDotPromoUserGroup::kOnlyBlueDotPromoEnabled ||
kBlueDotPromoUserGroupParam.Get() ==
BlueDotPromoUserGroup::kAllDBPromosEnabled;
}
return false;
}
bool IsDefaultBrowserInPromoManagerEnabled() {
return base::FeatureList::IsEnabled(kDefaultBrowserRefactoringPromoManager);
}
bool NonModalPromosEnabled() {
// Default browser isn't enabled until iOS 14.0.1, regardless of flag state.
return base::ios::IsRunningOnOrLater(14, 0, 1);
}
bool HasUserInteractedWithFullscreenPromoBefore() {
NSNumber* number = GetObjectFromStorageForKey<NSNumber>(
kUserHasInteractedWithFullscreenPromo);
return number.boolValue;
}
bool HasUserInteractedWithTailoredFullscreenPromoBefore() {
NSNumber* number = GetObjectFromStorageForKey<NSNumber>(
kUserHasInteractedWithTailoredFullscreenPromo);
return number.boolValue;
}
bool HasUserOpenedSettingsFromFirstRunPromo() {
NSNumber* number =
GetObjectFromStorageForKey<NSNumber>(kOpenSettingsActionInteraction);
return number.boolValue;
}
NSInteger UserInteractionWithNonModalPromoCount() {
NSNumber* number = GetObjectFromStorageForKey<NSNumber>(
kUserInteractedWithNonModalPromoCount);
return number.integerValue;
}
void LogUserInteractionWithFullscreenPromo() {
const NSInteger displayed_promo_count = DisplayedPromoCount();
NSDictionary<NSString*, NSObject*>* update = @{
kUserHasInteractedWithFullscreenPromo : @YES,
kLastTimeUserInteractedWithPromo : [NSDate date],
kDisplayedPromoCount : @(displayed_promo_count + 1),
};
if (IsInRemindMeLaterGroup()) {
// Clear any possible Remind Me Later timestamp saved.
NSMutableDictionary<NSString*, NSObject*>* copy = [update mutableCopy];
copy[kRemindMeLaterPromoActionInteraction] = [NSNull null];
update = copy;
}
UpdateStorageWithDictionary(update);
}
void LogUserInteractionWithTailoredFullscreenPromo() {
const NSInteger displayed_promo_count = DisplayedPromoCount();
UpdateStorageWithDictionary(@{
kUserHasInteractedWithTailoredFullscreenPromo : @YES,
kLastTimeUserInteractedWithPromo : [NSDate date],
kDisplayedPromoCount : @(displayed_promo_count + 1),
});
}
void LogUserInteractionWithNonModalPromo() {
const NSInteger interaction_count = UserInteractionWithNonModalPromoCount();
const NSInteger displayed_promo_count = DisplayedPromoCount();
UpdateStorageWithDictionary(@{
kLastTimeUserInteractedWithPromo : [NSDate date],
kUserInteractedWithNonModalPromoCount : @(interaction_count + 1),
kDisplayedPromoCount : @(displayed_promo_count + 1),
});
}
void LogUserInteractionWithFirstRunPromo(BOOL openedSettings) {
const NSInteger displayed_promo_count = DisplayedPromoCount();
UpdateStorageWithDictionary(@{
kUserHasInteractedWithFirstRunPromo : @YES,
kLastTimeUserInteractedWithPromo : [NSDate date],
kDisplayedPromoCount : @(displayed_promo_count + 1),
});
}
bool HasRecentFirstPartyIntentLaunchesAndRecordsCurrentLaunch() {
if (HasRecordedEventForKeyLessThanDelay(
kTimestampAppLastOpenedViaFirstPartyIntent,
kMaximumTimeBetweenFirstPartyAppLaunches)) {
if (HasRecordedEventForKeyMoreThanDelay(
kTimestampAppLastOpenedViaFirstPartyIntent,
kMaximumTimeOneUserSession)) {
SetObjectIntoStorageForKey(kTimestampAppLastOpenedViaFirstPartyIntent,
[NSDate date]);
return YES;
}
return NO;
}
SetObjectIntoStorageForKey(kTimestampAppLastOpenedViaFirstPartyIntent,
[NSDate date]);
return NO;
}
bool HasRecentValidURLPastesAndRecordsCurrentPaste() {
if (HasRecordedEventForKeyLessThanDelay(kTimestampLastValidURLPasted,
kMaximumTimeBetweenValidURLPastes)) {
SetObjectIntoStorageForKey(kTimestampLastValidURLPasted, [NSDate date]);
return YES;
}
SetObjectIntoStorageForKey(kTimestampLastValidURLPasted, [NSDate date]);
return NO;
}
bool HasRecentTimestampForKey(NSString* eventKey) {
if (HasRecordedEventForKeyLessThanDelay(eventKey,
kMaximumTimeOneUserSession)) {
return YES;
}
SetObjectIntoStorageForKey(eventKey, [NSDate date]);
return NO;
}
bool IsChromeLikelyDefaultBrowser7Days() {
return HasRecordedEventForKeyLessThanDelay(kLastHTTPURLOpenTime,
base::Days(7));
}
bool IsChromeLikelyDefaultBrowser() {
return HasRecordedEventForKeyLessThanDelay(kLastHTTPURLOpenTime,
kLatestURLOpenForDefaultBrowser);
}
bool IsLikelyInterestedDefaultBrowserUser(DefaultPromoType promo_type) {
std::vector<base::Time> times = LoadTimestampsForPromoType(promo_type);
return !times.empty();
}
DefaultPromoType MostRecentInterestDefaultPromoType(
BOOL skip_all_tabs_promo_type) {
DefaultPromoType most_recent_event_type = DefaultPromoTypeGeneral;
base::Time most_recent_event_time = base::Time::Min();
for (DefaultPromoType promo_type : kDefaultPromoTypes) {
// Ignore DefaultPromoTypeAllTabs if the extra requirements are not met.
if (promo_type == DefaultPromoTypeAllTabs && skip_all_tabs_promo_type) {
continue;
}
std::vector<base::Time> times = LoadTimestampsForPromoType(promo_type);
if (times.empty()) {
continue;
}
const base::Time last_time_for_type = times.back();
if (last_time_for_type >= most_recent_event_time) {
most_recent_event_type = promo_type;
most_recent_event_time = last_time_for_type;
}
}
return most_recent_event_type;
}
bool UserInPromoCooldown() {
return HasRecordedEventForKeyLessThanDelay(kLastTimeUserInteractedWithPromo,
ComputeCooldown());
}
// 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,
kLastTimeUserInteractedWithPromo,
kUserHasInteractedWithFullscreenPromo,
kUserHasInteractedWithTailoredFullscreenPromo,
kUserHasInteractedWithFirstRunPromo,
kUserInteractedWithNonModalPromoCount,
kDisplayedPromoCount,
kRemindMeLaterPromoActionInteraction,
kOpenSettingsActionInteraction,
// clang-format on
];
return keysForTesting;
}