blob: eb8661a4bd73369deeb0518cf8350e9864f70c08 [file] [log] [blame]
// Copyright 2025 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/tips_notifications/model/tips_notification_criteria.h"
#import "base/time/time.h"
#import "components/feature_engagement/public/tracker.h"
#import "components/password_manager/core/browser/password_manager_util.h"
#import "components/prefs/pref_service.h"
#import "components/safe_browsing/core/common/safe_browsing_prefs.h"
#import "components/search/search.h"
#import "components/sync/service/sync_service.h"
#import "components/sync/service/sync_user_settings.h"
#import "ios/chrome/browser/content_suggestions/ui_bundled/set_up_list/utils.h"
#import "ios/chrome/browser/default_browser/model/promo_source.h"
#import "ios/chrome/browser/default_browser/model/utils.h"
#import "ios/chrome/browser/feature_engagement/model/tracker_factory.h"
#import "ios/chrome/browser/lens/ui_bundled/lens_availability.h"
#import "ios/chrome/browser/ntp/model/features.h"
#import "ios/chrome/browser/ntp/model/set_up_list_prefs.h"
#import "ios/chrome/browser/search_engines/model/template_url_service_factory.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/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/model/utils/first_run_util.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/signin/model/authentication_service.h"
#import "ios/chrome/browser/signin/model/authentication_service_factory.h"
#import "ios/chrome/browser/sync/model/sync_service_factory.h"
#import "ios/chrome/browser/tips_notifications/model/utils.h"
#import "ui/base/device_form_factor.h"
using base::Time;
using base::TimeDelta;
namespace {
// The amount of time used to determine if Lens was opened recently.
const TimeDelta kLensOpenedRecency = base::Days(30);
// The amount of time used to determine if the user used Lens Overlay recently.
const TimeDelta kLensOverlayRecency = base::Days(30);
// The amount of time used to determine if the CPE promo was displayed recently.
const TimeDelta kCPEPromoRecency = base::Days(7);
// The amount of time used to determine if the user successfully logged in
// recently.
const TimeDelta kSuccessfullLoginRecency = base::Days(30);
// Returns true if a Default Browser Promo was canceled.
bool DefaultBrowserPromoCanceled() {
std::optional<IOSDefaultBrowserPromoAction> action =
DefaultBrowserPromoLastAction();
if (!action.has_value()) {
return false;
}
switch (action.value()) {
case IOSDefaultBrowserPromoAction::kCancel:
return true;
case IOSDefaultBrowserPromoAction::kActionButton:
case IOSDefaultBrowserPromoAction::kRemindMeLater:
case IOSDefaultBrowserPromoAction::kDismiss:
return false;
}
}
// Returns true if `time` is less time ago than `delta`.
bool IsRecent(base::Time time, TimeDelta delta) {
return base::Time::Now() - time < delta;
}
} // namespace
TipsNotificationCriteria::TipsNotificationCriteria(ProfileIOS* profile,
PrefService* local_state,
bool reactivation)
: profile_(profile),
profile_prefs_(profile->GetPrefs()),
local_state_(local_state),
reactivation_(reactivation) {}
bool TipsNotificationCriteria::ShouldSendNotification(
TipsNotificationType type) {
switch (type) {
case TipsNotificationType::kDefaultBrowser:
return ShouldSendDefaultBrowser();
case TipsNotificationType::kWhatsNew:
return ShouldSendWhatsNew();
case TipsNotificationType::kSignin:
return ShouldSendSignin();
case TipsNotificationType::kSetUpListContinuation:
return ShouldSendSetUpListContinuation();
case TipsNotificationType::kDocking:
return ShouldSendDocking();
case TipsNotificationType::kOmniboxPosition:
return ShouldSendOmniboxPosition();
case TipsNotificationType::kLens:
return ShouldSendLens();
case TipsNotificationType::kEnhancedSafeBrowsing:
return ShouldSendEnhancedSafeBrowsing();
case TipsNotificationType::kCPE:
return ShouldSendCPE();
case TipsNotificationType::kLensOverlay:
return ShouldSendLensOverlay();
case TipsNotificationType::kTrustedVaultKeyRetrieval:
return ShouldSendTrustedVaultKeyRetrieval();
case TipsNotificationType::kIncognitoLock:
case TipsNotificationType::kError:
NOTREACHED();
}
}
bool TipsNotificationCriteria::CanSignIn(ProfileIOS* profile) {
AuthenticationService* auth_service =
AuthenticationServiceFactory::GetForProfile(profile);
return auth_service->SigninEnabled() &&
!auth_service->HasPrimaryIdentity(signin::ConsentLevel::kSignin);
}
bool TipsNotificationCriteria::ShouldSendDefaultBrowser() {
if (IsChromeLikelyDefaultBrowser()) {
return false;
}
if (base::FeatureList::IsEnabled(kIOSOneTimeDefaultBrowserNotification)) {
// For kIOSOneTimeDefaultBrowserNotification, the logic simply checks if
// the user has not seen the promo in the last 14 days.
feature_engagement::Tracker* tracker =
feature_engagement::TrackerFactory::GetForProfile(profile_);
bool would_trigger = tracker->WouldTriggerHelpUI(
feature_engagement::kIPHiOSOneTimeDefaultBrowserNotificationFeature);
if (would_trigger) {
return true;
}
}
return !DefaultBrowserPromoCanceled();
}
bool TipsNotificationCriteria::ShouldSendWhatsNew() {
return !FETHasEverTriggered(feature_engagement::kIPHWhatsNewUpdatedFeature);
}
bool TipsNotificationCriteria::ShouldSendSignin() {
return CanSignIn(profile_);
}
bool TipsNotificationCriteria::ShouldSendSetUpListContinuation() {
if (!set_up_list_utils::IsSetUpListActive(local_state_, profile_prefs_)) {
return false;
}
// This notification should only be requested during the duration of the Set
// Up List minus the trigger interval after FirstRun.
TimeDelta trigger_delta = TipsNotificationTriggerDelta(
reactivation_, GetTipsNotificationUserType(local_state_));
if (!IsFirstRunRecent(set_up_list::SetUpListDurationPastFirstRun() -
trigger_delta)) {
return false;
}
return !set_up_list_prefs::AllItemsComplete(local_state_);
}
bool TipsNotificationCriteria::ShouldSendDocking() {
return !FETHasEverTriggered(feature_engagement::kIPHiOSDockingPromoFeature) &&
!FETHasEverTriggered(
feature_engagement::kIPHiOSDockingPromoRemindMeLaterFeature);
}
bool TipsNotificationCriteria::ShouldSendOmniboxPosition() {
// OmniboxPositionChoice is only available on phones.
if (ui::GetDeviceFormFactor() != ui::DEVICE_FORM_FACTOR_PHONE) {
return false;
}
return !local_state_->GetUserPrefValue(prefs::kBottomOmnibox);
}
bool TipsNotificationCriteria::ShouldSendLens() {
// Early return if Lens is not available or disabled by policy.
TemplateURLService* template_url_service =
ios::TemplateURLServiceFactory::GetForProfile(profile_);
bool default_search_is_google =
search::DefaultSearchProviderIsGoogle(template_url_service);
const bool lens_enabled =
lens_availability::CheckAndLogAvailabilityForLensEntryPoint(
LensEntrypoint::NewTabPage, default_search_is_google);
if (!lens_enabled) {
return false;
}
base::Time last_opened = local_state_->GetTime(prefs::kLensLastOpened);
return !IsRecent(last_opened, kLensOpenedRecency);
}
bool TipsNotificationCriteria::ShouldSendEnhancedSafeBrowsing() {
return profile_prefs_->GetBoolean(prefs::kAdvancedProtectionAllowed) &&
!safe_browsing::IsEnhancedProtectionEnabled(*profile_prefs_);
}
bool TipsNotificationCriteria::ShouldSendCPE() {
if (!local_state_->GetBoolean(
prefs::kIosCredentialProviderPromoPolicyEnabled)) {
return false;
}
bool is_credential_provider_enabled =
password_manager_util::IsCredentialProviderEnabledOnStartup(local_state_);
if (is_credential_provider_enabled) {
return false;
}
base::Time promo_display_time =
local_state_->GetTime(prefs::kIosCredentialProviderPromoDisplayTime);
if (IsRecent(promo_display_time, kCPEPromoRecency)) {
return false;
}
base::Time login_time =
local_state_->GetTime(prefs::kIosSuccessfulLoginWithExistingPassword);
return IsRecent(login_time, kSuccessfullLoginRecency);
}
bool TipsNotificationCriteria::ShouldSendLensOverlay() {
base::Time lens_overlay_last_presented =
local_state_->GetTime(prefs::kLensOverlayLastPresented);
return !IsRecent(lens_overlay_last_presented, kLensOverlayRecency);
}
bool TipsNotificationCriteria::ShouldSendTrustedVaultKeyRetrieval() {
syncer::SyncService* sync_service =
SyncServiceFactory::GetForProfileIfExists(profile_);
if (sync_service == nullptr) {
// Sync service might be not available for some profiles. If Sync service is
// not available, we don't need to do display this notification.
return false;
}
if (!IsIOSTrustedVaultNotificationEnabled()) {
return false;
}
return sync_service->GetUserSettings()
->IsTrustedVaultKeyRequiredForPreferredDataTypes();
}
bool TipsNotificationCriteria::FETHasEverTriggered(
const base::Feature& feature) {
feature_engagement::Tracker* tracker =
feature_engagement::TrackerFactory::GetForProfile(profile_);
return tracker->HasEverTriggered(feature, true);
}