blob: d57d7db507b34f437e3f5d166134ee18c764df9a [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/promos/promos_utils.h"
#include <string>
#include <vector>
#include "base/containers/contains.h"
#include "base/feature_list.h"
#include "base/json/values_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/time/time.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/promos/promos_pref_names.h"
#include "chrome/browser/promos/promos_types.h"
#include "chrome/common/pref_names.h"
#include "components/feature_engagement/public/feature_constants.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/search/ntp_features.h"
#include "components/segmentation_platform/embedder/default_model/device_switcher_model.h"
#include "components/sync/base/data_type.h"
#include "components/sync/service/sync_service.h"
namespace promos_utils {
// Max impression count per user, per promo for the iOS desktop promos on
// desktop.
constexpr int kiOSDesktopPromoMaxImpressionCount = 3;
// Total impression count per user in their lifetime, for all iOS desktop
// promos.
constexpr int kiOSDesktopPromoTotalImpressionCount = 10;
// Total amount of opt-outs across any Desktop to iOS promo to block impressions
// of other instances of Desktop to iOS promos, per user.
constexpr int kiOSDesktopPromoTotalOptOuts = 2;
// Minimum time threshold between impressions for a given user to see the iOS
// desktop promo on desktop.
constexpr base::TimeDelta kiOSDesktopPromoCooldownTime = base::Days(90);
// IOSDesktopPromoHistogramType returns the promo histogram type for the given
// promo type. New promos should add themselves to this check.
std::string IOSDesktopPromoHistogramType(IOSPromoType promo_type) {
switch (promo_type) {
case IOSPromoType::kPassword:
return "PasswordPromo";
case IOSPromoType::kAddress:
return "AddressPromo";
case IOSPromoType::kPayment:
return "PaymentPromo";
}
}
base::Time GetMostRecentiOSDesktopNtpPromoTimestamp(PrefService* pref_service) {
const base::Value::List& timestamps = pref_service->GetList(
promos_prefs::kDesktopToiOSNtpPromoAppearanceTimestamps);
base::Value::List::const_iterator most_recent_timestamp_iter =
std::max_element(timestamps.begin(), timestamps.end());
if (most_recent_timestamp_iter == timestamps.end()) {
return base::Time();
}
return base::ValueToTime(*most_recent_timestamp_iter).value_or(base::Time());
}
// VerifyIOSDesktopPromoTotalImpressions ensures that each individual user sees
// no more than a maximum of these promos total in their lifetime. New promos
// should add themselves to this check. If `skip_ntp_promo` is true, then this
// call will not include ntp promo data in its checks. This allows calling code
// from the NTP promo to add special logic.
bool VerifyIOSDesktopPromoTotalImpressions(Profile* profile,
bool skip_ntp_promo = false) {
int total_desktop_promo_impressions =
profile->GetPrefs()->GetInteger(
promos_prefs::kDesktopToiOSPasswordPromoImpressionsCounter) +
profile->GetPrefs()->GetInteger(
promos_prefs::kDesktopToiOSAddressPromoImpressionsCounter) +
profile->GetPrefs()->GetInteger(
promos_prefs::kDesktopToiOSPaymentPromoImpressionsCounter);
if (!skip_ntp_promo) {
// The Desktop NTP promo shows 10 times in quick succession, but that only
// counts as 1 impression for Desktop to iOS promos in general.
const base::Value::List& ntp_promo_appearances =
profile->GetPrefs()->GetList(
promos_prefs::kDesktopToiOSNtpPromoAppearanceTimestamps);
total_desktop_promo_impressions += (ntp_promo_appearances.empty()) ? 0 : 1;
}
return total_desktop_promo_impressions < kiOSDesktopPromoTotalImpressionCount;
}
// VerifyIOSDesktopPromoTotalOptOuts verifies that a user hasn't opted-out of
// seeing more than the allowed amount of opt-outs for all iOS Desktop promos.
// New promos should add themselves to this check.
bool VerifyIOSDesktopPromoTotalOptOuts(Profile* profile) {
std::vector<bool> promo_opt_outs = {
profile->GetPrefs()->GetBoolean(
promos_prefs::kDesktopToiOSPasswordPromoOptOut),
profile->GetPrefs()->GetBoolean(
promos_prefs::kDesktopToiOSAddressPromoOptOut),
profile->GetPrefs()->GetBoolean(
promos_prefs::kDesktopToiOSPaymentPromoOptOut)};
int total_desktop_promo_opt_outs_counter =
std::count(promo_opt_outs.begin(), promo_opt_outs.end(), true);
return total_desktop_promo_opt_outs_counter < kiOSDesktopPromoTotalOptOuts;
}
// VerifyMostRecentPromoTimestamp ensures that each individual user sees a
// Desktop to iOS promo a maximum of once per cooldown period. New promos should
// add themselves to this check. If `skip_ntp_promo` is true, then this call
// will not include ntp promo data in its checks. This allows calling code from
// the NTP promo to add special logic.
bool VerifyMostRecentPromoTimestamp(Profile* profile,
bool skip_ntp_promo = false) {
std::vector<base::Time> promos_timestamps = {
profile->GetPrefs()->GetTime(
promos_prefs::kDesktopToiOSPasswordPromoLastImpressionTimestamp),
profile->GetPrefs()->GetTime(
promos_prefs::kDesktopToiOSAddressPromoLastImpressionTimestamp),
profile->GetPrefs()->GetTime(
promos_prefs::kDesktopToiOSPaymentPromoLastImpressionTimestamp),
};
if (!skip_ntp_promo) {
promos_timestamps.emplace_back(
GetMostRecentiOSDesktopNtpPromoTimestamp(profile->GetPrefs()));
}
auto most_recent_promo_timestamp =
std::max_element(promos_timestamps.begin(), promos_timestamps.end());
return *most_recent_promo_timestamp + kiOSDesktopPromoCooldownTime <
base::Time::Now();
}
// Verify that the user is syncing preferences (for impressions and opt-out
// tracking), and that they are syncing the specific datatype needed for a given
// promo type.
bool VerifySyncingDatatypes(const syncer::SyncService& sync_service,
IOSPromoType promo_type) {
if (!sync_service.GetActiveDataTypes().Has(syncer::PREFERENCES)) {
return false;
}
switch (promo_type) {
case IOSPromoType::kPassword:
return sync_service.GetActiveDataTypes().Has(syncer::PASSWORDS);
case IOSPromoType::kAddress:
return sync_service.GetActiveDataTypes().Has(syncer::CONTACT_INFO);
case IOSPromoType::kPayment:
return sync_service.GetActiveDataTypes().Has(
syncer::AUTOFILL_WALLET_DATA);
}
}
// Checks whether promos in general can currently be shown.
bool CanShowPromos() {
// Don't show the promo if the local state exists and `kPromotionsEnabled` is
// false (likely overridden by policy). `kPromotionsEnabled` does not exist on
// Android.
#if !BUILDFLAG(IS_ANDROID)
PrefService* local_state = g_browser_process->local_state();
if (local_state && !local_state->GetBoolean(prefs::kPromotionsEnabled)) {
return false;
}
#endif // !BUILDFLAG(IS_ANDROID)
return true;
}
// RecordIOSDesktopPromoShownHistogram records which impression (count) was
// shown to the user depending on the given promo type.
void RecordIOSDesktopPromoShownHistogram(IOSPromoType promo_type,
int impression_count) {
std::string promo_histogram_type = IOSDesktopPromoHistogramType(promo_type);
DesktopIOSPromoImpression promo_impression;
switch (impression_count) {
case 1:
promo_impression = DesktopIOSPromoImpression::kFirstImpression;
break;
case 2:
promo_impression = DesktopIOSPromoImpression::kSecondImpression;
break;
case 3:
promo_impression = DesktopIOSPromoImpression::kThirdImpression;
break;
default:
NOTREACHED();
}
base::UmaHistogramEnumeration(
"IOS.Desktop." + promo_histogram_type + ".Shown", promo_impression);
}
// IOSPromoPrefsConfig is a complex struct that needs definition of a
// constructor, an explicit out-of-line copy constructor and a destructor. New
// promos should add themselves to this function.
IOSPromoPrefsConfig::IOSPromoPrefsConfig() = default;
IOSPromoPrefsConfig::IOSPromoPrefsConfig(const IOSPromoPrefsConfig&) = default;
IOSPromoPrefsConfig::~IOSPromoPrefsConfig() = default;
IOSPromoPrefsConfig::IOSPromoPrefsConfig(IOSPromoType promo_type) {
switch (promo_type) {
case IOSPromoType::kPassword:
#if !BUILDFLAG(IS_ANDROID)
promo_feature = &feature_engagement::kIPHiOSPasswordPromoDesktopFeature;
#endif // !BUILDFLAG(IS_ANDROID)
promo_impressions_counter_pref_name =
promos_prefs::kDesktopToiOSPasswordPromoImpressionsCounter;
promo_opt_out_pref_name = promos_prefs::kDesktopToiOSPasswordPromoOptOut;
promo_last_impression_timestamp_pref_name =
promos_prefs::kDesktopToiOSPasswordPromoLastImpressionTimestamp;
break;
case IOSPromoType::kAddress:
#if !BUILDFLAG(IS_ANDROID)
promo_feature = &feature_engagement::kIPHiOSAddressPromoDesktopFeature;
#endif // !BUILDFLAG(IS_ANDROID)
promo_impressions_counter_pref_name =
promos_prefs::kDesktopToiOSAddressPromoImpressionsCounter;
promo_opt_out_pref_name = promos_prefs::kDesktopToiOSAddressPromoOptOut;
promo_last_impression_timestamp_pref_name =
promos_prefs::kDesktopToiOSAddressPromoLastImpressionTimestamp;
break;
case IOSPromoType::kPayment:
#if !BUILDFLAG(IS_ANDROID)
promo_feature = &feature_engagement::kIPHiOSPaymentPromoDesktopFeature;
#endif // !BUILDFLAG(IS_ANDROID)
promo_impressions_counter_pref_name =
promos_prefs::kDesktopToiOSPaymentPromoImpressionsCounter;
promo_opt_out_pref_name = promos_prefs::kDesktopToiOSPaymentPromoOptOut;
promo_last_impression_timestamp_pref_name =
promos_prefs::kDesktopToiOSPaymentPromoLastImpressionTimestamp;
break;
}
}
// Registers profile prefs. New promos should add themselves to this function.
void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry) {
registry->RegisterTimePref(
promos_prefs::kDesktopToiOSPasswordPromoLastImpressionTimestamp,
base::Time(), user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
registry->RegisterIntegerPref(
promos_prefs::kDesktopToiOSPasswordPromoImpressionsCounter, 0,
user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
registry->RegisterBooleanPref(
promos_prefs::kDesktopToiOSPasswordPromoOptOut, false,
user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
registry->RegisterTimePref(
promos_prefs::kDesktopToiOSAddressPromoLastImpressionTimestamp,
base::Time(), user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
registry->RegisterIntegerPref(
promos_prefs::kDesktopToiOSAddressPromoImpressionsCounter, 0,
user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
registry->RegisterBooleanPref(
promos_prefs::kDesktopToiOSAddressPromoOptOut, false,
user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
registry->RegisterTimePref(
promos_prefs::kDesktopToiOSPaymentPromoLastImpressionTimestamp,
base::Time(), user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
registry->RegisterIntegerPref(
promos_prefs::kDesktopToiOSPaymentPromoImpressionsCounter, 0,
user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
registry->RegisterBooleanPref(
promos_prefs::kDesktopToiOSPaymentPromoOptOut, false,
user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
registry->RegisterListPref(
promos_prefs::kDesktopToiOSNtpPromoAppearanceTimestamps, {},
user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
registry->RegisterBooleanPref(
promos_prefs::kDesktopToiOSNtpPromoDismissed, false,
user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
}
const base::Feature& GetIOSDesktopPromoFeatureEngagement(
IOSPromoType promo_type) {
IOSPromoPrefsConfig promo_prefs(promo_type);
return *promo_prefs.promo_feature;
}
// RecordIOSDesktopPromoUserInteractionHistogram records which impression
// (count) depending on the promo type.
void RecordIOSDesktopPromoUserInteractionHistogram(
IOSPromoType promo_type,
int impression_count,
DesktopIOSPromoAction action) {
std::string promo_histogram_type = IOSDesktopPromoHistogramType(promo_type);
if (impression_count == 1) {
base::UmaHistogramEnumeration(
"IOS.Desktop." + promo_histogram_type + ".FirstImpression.Action",
action);
} else if (impression_count == 2) {
base::UmaHistogramEnumeration(
"IOS.Desktop." + promo_histogram_type + ".SecondImpression.Action",
action);
} else if (impression_count == 3) {
base::UmaHistogramEnumeration(
"IOS.Desktop." + promo_histogram_type + ".ThirdImpression.Action",
action);
} else {
NOTREACHED();
}
}
bool ShouldShowIOSDesktopPromo(Profile* profile,
const syncer::SyncService* sync_service,
IOSPromoType promo_type) {
if (!CanShowPromos()) {
return false;
}
IOSPromoPrefsConfig promo_prefs(promo_type);
// Show the promo if the user hasn't opted out, is not in the cooldown
// period and is within the impression limit for this promo.
return sync_service && VerifySyncingDatatypes(*sync_service, promo_type) &&
profile->GetPrefs()->GetInteger(
promo_prefs.promo_impressions_counter_pref_name) <
kiOSDesktopPromoMaxImpressionCount &&
VerifyMostRecentPromoTimestamp(profile) &&
VerifyIOSDesktopPromoTotalImpressions(profile) &&
VerifyIOSDesktopPromoTotalOptOuts(profile) &&
!profile->GetPrefs()->GetBoolean(promo_prefs.promo_opt_out_pref_name);
}
bool ShouldShowIOSDesktopNtpPromo(Profile* profile,
const syncer::SyncService* sync_service) {
if (!CanShowPromos()) {
return false;
}
PrefService* pref_service = profile->GetPrefs();
// Sync must be active because prefs to track promo display are synced.
bool sync_ok = sync_service &&
sync_service->GetActiveDataTypes().Has(syncer::PREFERENCES);
// This promo can appear 10 times itself.
int appearance_count =
pref_service
->GetList(promos_prefs::kDesktopToiOSNtpPromoAppearanceTimestamps)
.size();
bool appearance_count_ok =
appearance_count < ntp_features::kNtpMobilePromoImpressionLimit.Get();
bool has_not_dismissed = !profile->GetPrefs()->GetBoolean(
promos_prefs::kDesktopToiOSNtpPromoDismissed);
bool other_promos_ok =
VerifyMostRecentPromoTimestamp(profile, /*skip_ntp_promo=*/true) &&
VerifyIOSDesktopPromoTotalImpressions(profile, /*skip_ntp_promo=*/true);
// Show the promo if the user hasn't opted out, is not in the cooldown
// period and is within the impression limit for this promo.
return sync_ok && appearance_count_ok && has_not_dismissed && other_promos_ok;
}
bool UserNotClassifiedAsMobileDeviceSwitcher(
const segmentation_platform::ClassificationResult& result) {
return result.status == segmentation_platform::PredictionStatus::kSucceeded &&
!base::Contains(
result.ordered_labels,
segmentation_platform::DeviceSwitcherModel::kAndroidPhoneLabel) &&
!base::Contains(result.ordered_labels,
segmentation_platform::DeviceSwitcherModel::
kIosPhoneChromeLabel) &&
!base::Contains(
result.ordered_labels,
segmentation_platform::DeviceSwitcherModel::kAndroidTabletLabel) &&
!base::Contains(
result.ordered_labels,
segmentation_platform::DeviceSwitcherModel::kIosTabletLabel);
}
void IOSDesktopPromoShown(Profile* profile, IOSPromoType promo_type) {
IOSPromoPrefsConfig promo_prefs(promo_type);
int new_impression_count =
profile->GetPrefs()->GetInteger(
promo_prefs.promo_impressions_counter_pref_name) +
1;
profile->GetPrefs()->SetInteger(
promo_prefs.promo_impressions_counter_pref_name, new_impression_count);
profile->GetPrefs()->SetTime(
promo_prefs.promo_last_impression_timestamp_pref_name, base::Time::Now());
RecordIOSDesktopPromoShownHistogram(promo_type, new_impression_count);
}
void IOSDesktopNtpPromoShown(PrefService* pref_service) {
ScopedListPrefUpdate update(
pref_service, promos_prefs::kDesktopToiOSNtpPromoAppearanceTimestamps);
update->Append(base::TimeToValue(base::Time::Now()));
}
} // namespace promos_utils