blob: 1efe68e286e86149d7b013de4b78624ee13a6549 [file] [log] [blame]
// Copyright 2024 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_client.h"
#import "base/metrics/histogram_functions.h"
#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "base/strings/sys_string_conversions.h"
#import "base/task/bind_post_task.h"
#import "base/time/time.h"
#import "components/feature_engagement/public/tracker.h"
#import "components/prefs/pref_registry_simple.h"
#import "components/prefs/pref_service.h"
#import "components/safe_browsing/core/common/safe_browsing_prefs.h"
#import "components/search/search.h"
#import "components/signin/public/identity_manager/identity_manager.h"
#import "ios/chrome/browser/authentication/ui_bundled/signin_presenter.h"
#import "ios/chrome/browser/content_suggestions/ui_bundled/content_suggestions_commands.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/push_notification/model/constants.h"
#import "ios/chrome/browser/push_notification/model/push_notification_client.h"
#import "ios/chrome/browser/push_notification/model/push_notification_service.h"
#import "ios/chrome/browser/push_notification/model/push_notification_util.h"
#import "ios/chrome/browser/search_engines/model/template_url_service_factory.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_state.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/browser/browser_list.h"
#import "ios/chrome/browser/shared/model/browser/browser_list_factory.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/model/utils/first_run_util.h"
#import "ios/chrome/browser/shared/public/commands/application_commands.h"
#import "ios/chrome/browser/shared/public/commands/browser_coordinator_commands.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/commands/docking_promo_commands.h"
#import "ios/chrome/browser/shared/public/commands/settings_commands.h"
#import "ios/chrome/browser/shared/public/commands/show_signin_command.h"
#import "ios/chrome/browser/shared/public/commands/whats_new_commands.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/signin/model/chrome_account_manager_service.h"
#import "ios/chrome/browser/signin/model/chrome_account_manager_service_factory.h"
#import "ios/chrome/browser/signin/model/identity_manager_factory.h"
#import "ios/chrome/browser/tips_notifications/model/utils.h"
#import "ios/public/provider/chrome/browser/lens/lens_api.h"
#import "ui/base/device_form_factor.h"
namespace {
// The amount of time used to determine if Lens was opened recently.
const base::TimeDelta kLensOpenedRecency = base::Days(30);
// Returns the first notification from `requests` whose identifier matches
// `identifier`.
UNNotificationRequest* NotificationWithIdentifier(
NSString* identifier,
NSArray<UNNotificationRequest*>* requests) {
for (UNNotificationRequest* request in requests) {
if ([request.identifier isEqualToString:identifier]) {
return request;
}
}
return nil;
}
// Returns true if signin is allowed / enabled.
bool IsSigninEnabled(AuthenticationService* auth_service) {
switch (auth_service->GetServiceStatus()) {
case AuthenticationService::ServiceStatus::SigninForcedByPolicy:
case AuthenticationService::ServiceStatus::SigninAllowed:
return true;
case AuthenticationService::ServiceStatus::SigninDisabledByUser:
case AuthenticationService::ServiceStatus::SigninDisabledByPolicy:
case AuthenticationService::ServiceStatus::SigninDisabledByInternal:
return false;
}
}
// 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 the Feature Engagement Tracker has ever triggered for the
// given `feature`.
bool FETHasEverTriggered(Browser* browser, const base::Feature& feature) {
feature_engagement::Tracker* tracker =
feature_engagement::TrackerFactory::GetForProfile(browser->GetProfile());
return tracker->HasEverTriggered(feature, true);
}
// Returns the user's type stored in local state prefs.
TipsNotificationUserType GetUserType(PrefService* local_state) {
return static_cast<TipsNotificationUserType>(
local_state->GetInteger(kTipsNotificationsUserType));
}
// Sets the user's type in local state prefs, and records a histogram with the
// type.
void SetUserType(PrefService* local_state, TipsNotificationUserType user_type) {
local_state->SetInteger(kTipsNotificationsUserType, int(user_type));
base::UmaHistogramEnumeration("IOS.Notifications.Tips.UserType", user_type);
}
} // namespace
TipsNotificationClient::TipsNotificationClient()
: PushNotificationClient(PushNotificationClientId::kTips,
PushNotificationClientScope::kAppWide) {
local_state_ = GetApplicationContext()->GetLocalState();
pref_change_registrar_.Init(local_state_);
PrefChangeRegistrar::NamedChangeCallback pref_callback = base::BindRepeating(
&TipsNotificationClient::OnPermittedPrefChanged, base::Unretained(this));
pref_change_registrar_.Add(prefs::kAppLevelPushNotificationPermissions,
pref_callback);
PrefChangeRegistrar::NamedChangeCallback auth_pref_callback =
base::BindRepeating(&TipsNotificationClient::OnAuthPrefChanged,
base::Unretained(this));
pref_change_registrar_.Add(prefs::kPushNotificationAuthorizationStatus,
auth_pref_callback);
permitted_ = IsPermitted();
user_type_ = GetUserType(local_state_);
}
TipsNotificationClient::~TipsNotificationClient() = default;
bool TipsNotificationClient::HandleNotificationInteraction(
UNNotificationResponse* response) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!IsTipsNotification(response.notification.request)) {
return false;
}
interacted_type_ = ParseTipsNotificationType(response.notification.request);
if (!interacted_type_.has_value()) {
base::UmaHistogramEnumeration("IOS.Notifications.Tips.Interaction",
TipsNotificationType::kError);
return false;
}
const char* histogram =
IsProactiveTipsNotification(response.notification.request)
? "IOS.Notifications.Tips.Proactive.Interaction"
: "IOS.Notifications.Tips.Interaction";
base::UmaHistogramEnumeration(histogram, interacted_type_.value());
// If the app is not yet foreground active, store the notification type and
// handle it later when the app becomes foreground active.
if (IsSceneLevelForegroundActive()) {
CheckAndMaybeRequestNotification(base::DoNothing());
}
return true;
}
void TipsNotificationClient::HandleNotificationInteraction(
TipsNotificationType type) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
Browser* browser = GetSceneLevelForegroundActiveBrowser();
CHECK(browser);
id<ApplicationCommands> application_handler =
HandlerForProtocol(browser->GetCommandDispatcher(), ApplicationCommands);
[application_handler
prepareToPresentModal:
base::CallbackToBlock(
base::BindOnce(&TipsNotificationClient::ShowUIForNotificationType,
weak_ptr_factory_.GetWeakPtr(), type, browser))];
// If a relavent feature is enabled and the user hasn't yet opted-in, and the
// current auth status is "authorized", interacting with a notification (which
// must have been sent provisionally) will be treated as a positive signal to
// opt in the user to this type of notification.
if ((IsProvisionalNotificationAlertEnabled() ||
IsIOSReactivationNotificationsEnabled()) &&
!permitted_) {
[PushNotificationUtil
getPermissionSettings:base::CallbackToBlock(base::BindOnce(
&TipsNotificationClient::OptInIfAuthorized,
weak_ptr_factory_.GetWeakPtr(),
browser->GetProfile()->AsWeakPtr()))];
}
}
void TipsNotificationClient::OptInIfAuthorized(
base::WeakPtr<ProfileIOS> weak_profile,
UNNotificationSettings* settings) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (settings.authorizationStatus != UNAuthorizationStatusAuthorized) {
return;
}
ProfileIOS* profile = weak_profile.get();
if (!profile) {
return;
}
AuthenticationService* authService =
AuthenticationServiceFactory::GetForProfile(profile);
id<SystemIdentity> identity =
authService->GetPrimaryIdentity(signin::ConsentLevel::kSignin);
const std::string& gaiaID = base::SysNSStringToUTF8(identity.gaiaID);
PushNotificationService* service =
GetApplicationContext()->GetPushNotificationService();
// Set `permitted_` here so that the OnPermittedPrefChanged exits early.
permitted_ = true;
service->SetPreference(base::SysUTF8ToNSString(gaiaID),
PushNotificationClientId::kTips, true);
}
std::optional<UIBackgroundFetchResult>
TipsNotificationClient::HandleNotificationReception(
NSDictionary<NSString*, id>* userInfo) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (![userInfo objectForKey:kTipsNotificationId]) {
return std::nullopt;
}
return UIBackgroundFetchResultNoData;
}
NSArray<UNNotificationCategory*>*
TipsNotificationClient::RegisterActionableNotifications() {
return @[];
}
void TipsNotificationClient::OnSceneActiveForegroundBrowserReady() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
OnSceneActiveForegroundBrowserReady(base::DoNothing());
}
void TipsNotificationClient::OnSceneActiveForegroundBrowserReady(
base::OnceClosure closure) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (user_type_ == TipsNotificationUserType::kUnknown &&
!CanSendReactivation()) {
ClassifyUser();
}
CheckAndMaybeRequestNotification(std::move(closure));
}
void TipsNotificationClient::CheckAndMaybeRequestNotification(
base::OnceClosure closure) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
permitted_ = IsPermitted();
if (interacted_type_.has_value()) {
GetApplicationContext()->GetLocalState()->ClearPref(
kTipsNotificationsDismissCount);
HandleNotificationInteraction(interacted_type_.value());
}
// If the user hasn't opted-in, exit early to avoid incurring the cost of
// checking delivered and requested notifications.
if (!permitted_ && !CanSendReactivation()) {
std::move(closure).Run();
return;
}
GetPendingRequest(
base::BindOnce(&TipsNotificationClient::OnPendingRequestFound,
weak_ptr_factory_.GetWeakPtr())
.Then(std::move(closure)));
}
// static
void TipsNotificationClient::RegisterLocalStatePrefs(
PrefRegistrySimple* registry) {
registry->RegisterIntegerPref(kTipsNotificationsSentPref, 0);
registry->RegisterIntegerPref(kTipsNotificationsLastSent, -1);
registry->RegisterIntegerPref(kTipsNotificationsLastTriggered, -1);
registry->RegisterTimePref(kTipsNotificationsLastRequestedTime, base::Time());
registry->RegisterIntegerPref(kTipsNotificationsUserType, 0);
registry->RegisterIntegerPref(kTipsNotificationsDismissCount, 0);
registry->RegisterIntegerPref(kReactivationNotificationsCanceledCount, 0);
}
void TipsNotificationClient::GetPendingRequest(
GetPendingRequestCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto completion = base::CallbackToBlock(base::BindPostTask(
base::SequencedTaskRunner::GetCurrentDefault(),
base::BindOnce(&NotificationWithIdentifier, kTipsNotificationId)
.Then(std::move(callback))));
[UNUserNotificationCenter.currentNotificationCenter
getPendingNotificationRequestsWithCompletionHandler:completion];
}
void TipsNotificationClient::OnPendingRequestFound(
UNNotificationRequest* request) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!request) {
MaybeLogTriggeredNotification();
MaybeLogDismissedNotification();
interacted_type_ = std::nullopt;
MaybeRequestNotification(base::DoNothing());
return;
}
MaybeLogDismissedNotification();
interacted_type_ = std::nullopt;
if (CanSendReactivation()) {
ClearAllRequestedNotifications();
std::optional<TipsNotificationType> type =
ParseTipsNotificationType(request);
if (type.has_value()) {
MarkNotificationTypeNotSent(type.value());
// Increment the Reactivation canceled count.
int canceled_count =
local_state_->GetInteger(kReactivationNotificationsCanceledCount) + 1;
local_state_->SetInteger(kReactivationNotificationsCanceledCount,
canceled_count);
}
MaybeRequestNotification(base::DoNothing());
}
}
void TipsNotificationClient::MaybeRequestNotification(
base::OnceClosure completion) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if ((!permitted_ && !CanSendReactivation()) || DismissLimitReached()) {
std::move(completion).Run();
return;
}
int sent_bitfield = local_state_->GetInteger(kTipsNotificationsSentPref);
int enabled_bitfield = TipsNotificationsEnabledBitfield();
// The types of notifications that could be sent will be evaluated in the
// order they appear in this array.
std::vector<TipsNotificationType> types =
TipsNotificationsTypesOrder(CanSendReactivation());
for (TipsNotificationType type : types) {
int bit = 1 << int(type);
if (sent_bitfield & bit) {
// This type of notification has already been sent.
continue;
}
if (!(enabled_bitfield & bit)) {
// This type of notification is not enabled.
continue;
}
if (ShouldSendNotification(type)) {
RequestNotification(type, std::move(completion));
return;
}
}
std::move(completion).Run();
}
void TipsNotificationClient::ClearAllRequestedNotifications() {
[UNUserNotificationCenter.currentNotificationCenter
removePendingNotificationRequestsWithIdentifiers:@[
kTipsNotificationId
]];
}
void TipsNotificationClient::RequestNotification(TipsNotificationType type,
base::OnceClosure completion) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (IsNotificationCollisionManagementEnabled()) {
ScheduledNotificationRequest request = {
kTipsNotificationId,
ContentForTipsNotificationType(type, CanSendReactivation()),
TipsNotificationTriggerDelta(CanSendReactivation(), user_type_)};
CheckRateLimitBeforeSchedulingNotification(
request,
base::BindPostTask(
base::SequencedTaskRunner::GetCurrentDefault(),
base::BindOnce(&TipsNotificationClient::OnNotificationRequested,
weak_ptr_factory_.GetWeakPtr(), type)
.Then(std::move(completion))));
MarkNotificationTypeSent(type);
return;
}
UNNotificationRequest* request = [UNNotificationRequest
requestWithIdentifier:kTipsNotificationId
content:ContentForTipsNotificationType(
type, CanSendReactivation())
trigger:[UNTimeIntervalNotificationTrigger
triggerWithTimeInterval:
TipsNotificationTriggerDelta(
CanSendReactivation(), user_type_)
.InSecondsF()
repeats:NO]];
auto completion_block = base::CallbackToBlock(base::BindPostTask(
base::SequencedTaskRunner::GetCurrentDefault(),
base::BindOnce(&TipsNotificationClient::OnNotificationRequested,
weak_ptr_factory_.GetWeakPtr(), type)
.Then(std::move(completion))));
[UNUserNotificationCenter.currentNotificationCenter
addNotificationRequest:request
withCompletionHandler:completion_block];
MarkNotificationTypeSent(type);
}
void TipsNotificationClient::OnNotificationRequested(TipsNotificationType type,
NSError* error) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (error) {
base::RecordAction(
base::UserMetricsAction("IOS.Notifications.Tips.NotSentError"));
}
}
bool TipsNotificationClient::ShouldSendNotification(TipsNotificationType type) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
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::kError:
NOTREACHED();
}
}
bool TipsNotificationClient::ShouldSendDefaultBrowser() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return !IsChromeLikelyDefaultBrowser() && !DefaultBrowserPromoCanceled();
}
bool TipsNotificationClient::ShouldSendWhatsNew() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
Browser* browser = GetSceneLevelForegroundActiveBrowser();
if (!browser) {
return false;
}
return !FETHasEverTriggered(browser,
feature_engagement::kIPHWhatsNewUpdatedFeature);
}
bool TipsNotificationClient::ShouldSendSignin() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
Browser* browser = GetSceneLevelForegroundActiveBrowser();
if (!browser) {
return false;
}
ProfileIOS* profile = browser->GetProfile();
AuthenticationService* auth_service =
AuthenticationServiceFactory::GetForProfile(profile);
return IsSigninEnabled(auth_service) &&
!auth_service->HasPrimaryIdentity(signin::ConsentLevel::kSignin);
}
bool TipsNotificationClient::ShouldSendSetUpListContinuation() {
Browser* browser = GetSceneLevelForegroundActiveBrowser();
if (!browser) {
return false;
}
PrefService* local_prefs = GetApplicationContext()->GetLocalState();
PrefService* user_prefs = browser->GetProfile()->GetPrefs();
if (!set_up_list_utils::IsSetUpListActive(local_prefs, user_prefs)) {
return false;
}
// This notification should only be requested during the duration of the Set
// Up List minus the trigger interval after FirstRun.
if (!IsFirstRunRecent(
set_up_list::SetUpListDurationPastFirstRun() -
TipsNotificationTriggerDelta(CanSendReactivation(), user_type_))) {
return false;
}
return !set_up_list_prefs::AllItemsComplete(local_prefs);
}
bool TipsNotificationClient::ShouldSendDocking() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
Browser* browser = GetSceneLevelForegroundActiveBrowser();
if (!browser) {
return false;
}
return !FETHasEverTriggered(browser,
feature_engagement::kIPHiOSDockingPromoFeature) &&
!FETHasEverTriggered(
browser,
feature_engagement::kIPHiOSDockingPromoRemindMeLaterFeature);
}
bool TipsNotificationClient::ShouldSendOmniboxPosition() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// OmniboxPositionChoice is only available on phones.
if (ui::GetDeviceFormFactor() != ui::DEVICE_FORM_FACTOR_PHONE) {
return false;
}
return !GetApplicationContext()->GetLocalState()->GetUserPrefValue(
prefs::kBottomOmnibox);
}
bool TipsNotificationClient::ShouldSendLens() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Early return if Lens is not available or disabled by policy.
Browser* browser = GetSceneLevelForegroundActiveBrowser();
if (!browser) {
return false;
}
TemplateURLService* template_url_service =
ios::TemplateURLServiceFactory::GetForProfile(browser->GetProfile());
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 =
GetApplicationContext()->GetLocalState()->GetTime(prefs::kLensLastOpened);
return base::Time::Now() - last_opened > kLensOpenedRecency;
}
bool TipsNotificationClient::ShouldSendEnhancedSafeBrowsing() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
Browser* browser = GetSceneLevelForegroundActiveBrowser();
if (!browser) {
return false;
}
PrefService* user_prefs = browser->GetProfile()->GetPrefs();
return user_prefs->GetBoolean(prefs::kAdvancedProtectionAllowed) &&
!safe_browsing::IsEnhancedProtectionEnabled(*user_prefs);
}
bool TipsNotificationClient::IsSceneLevelForegroundActive() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return GetSceneLevelForegroundActiveBrowser() != nullptr;
}
void TipsNotificationClient::ShowUIForNotificationType(
TipsNotificationType type,
Browser* browser) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
switch (type) {
case TipsNotificationType::kDefaultBrowser:
ShowDefaultBrowserPromo(browser);
break;
case TipsNotificationType::kWhatsNew:
ShowWhatsNew(browser);
break;
case TipsNotificationType::kSignin:
ShowSignin(browser);
break;
case TipsNotificationType::kSetUpListContinuation:
ShowSetUpListContinuation(browser);
break;
case TipsNotificationType::kDocking:
ShowDocking(browser);
break;
case TipsNotificationType::kOmniboxPosition:
ShowOmniboxPosition(browser);
break;
case TipsNotificationType::kLens:
ShowLensPromo(browser);
break;
case TipsNotificationType::kEnhancedSafeBrowsing:
ShowEnhancedSafeBrowsingPromo(browser);
break;
case TipsNotificationType::kError:
NOTREACHED();
}
}
void TipsNotificationClient::ShowDefaultBrowserPromo(Browser* browser) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
id<SettingsCommands> settings_handler =
HandlerForProtocol(browser->GetCommandDispatcher(), SettingsCommands);
[settings_handler
showDefaultBrowserSettingsFromViewController:nil
sourceForUMA:
DefaultBrowserSettingsPageSource::
kTipsNotification];
}
void TipsNotificationClient::ShowWhatsNew(Browser* browser) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
[HandlerForProtocol(browser->GetCommandDispatcher(), WhatsNewCommands)
showWhatsNew];
}
void TipsNotificationClient::ShowSignin(Browser* browser) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// If there are 0 identities, kInstantSignin requires less taps.
AuthenticationOperation operation =
HasIdentitiesOnDevice(browser->GetProfile())
? AuthenticationOperation::kSigninOnly
: AuthenticationOperation::kInstantSignin;
ShowSigninCommand* command = [[ShowSigninCommand alloc]
initWithOperation:operation
identity:nil
accessPoint:signin_metrics::AccessPoint::kTipsNotification
promoAction:signin_metrics::PromoAction::
PROMO_ACTION_NO_SIGNIN_PROMO
completion:nil];
[HandlerForProtocol(browser->GetCommandDispatcher(), SigninPresenter)
showSignin:command];
}
void TipsNotificationClient::ShowSetUpListContinuation(Browser* browser) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
[HandlerForProtocol(browser->GetCommandDispatcher(),
ContentSuggestionsCommands)
showSetUpListSeeMoreMenuExpanded:YES];
}
void TipsNotificationClient::ShowDocking(Browser* browser) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
[HandlerForProtocol(browser->GetCommandDispatcher(), DockingPromoCommands)
showDockingPromoWithTrigger:DockingPromoTrigger::kTipsModule];
}
void TipsNotificationClient::ShowOmniboxPosition(Browser* browser) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
[HandlerForProtocol(browser->GetCommandDispatcher(),
BrowserCoordinatorCommands) showOmniboxPositionChoice];
}
void TipsNotificationClient::ShowLensPromo(Browser* browser) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
[HandlerForProtocol(browser->GetCommandDispatcher(),
BrowserCoordinatorCommands) showLensPromo];
}
void TipsNotificationClient::ShowEnhancedSafeBrowsingPromo(Browser* browser) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
[HandlerForProtocol(browser->GetCommandDispatcher(),
BrowserCoordinatorCommands)
showEnhancedSafeBrowsingPromo];
}
void TipsNotificationClient::MarkNotificationTypeSent(
TipsNotificationType type) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
int sent_bitfield = local_state_->GetInteger(kTipsNotificationsSentPref);
sent_bitfield |= 1 << int(type);
local_state_->SetInteger(kTipsNotificationsSentPref, sent_bitfield);
local_state_->SetInteger(kTipsNotificationsLastSent, int(type));
local_state_->SetTime(kTipsNotificationsLastRequestedTime, base::Time::Now());
const char* histogram = CanSendReactivation()
? "IOS.Notifications.Tips.Proactive.Sent"
: "IOS.Notifications.Tips.Sent";
base::UmaHistogramEnumeration(histogram, type);
}
void TipsNotificationClient::MarkNotificationTypeNotSent(
TipsNotificationType type) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
int sent_bitfield = local_state_->GetInteger(kTipsNotificationsSentPref);
sent_bitfield &= ~(1 << int(type));
local_state_->SetInteger(kTipsNotificationsSentPref, sent_bitfield);
local_state_->ClearPref(kTipsNotificationsLastSent);
}
void TipsNotificationClient::MaybeLogTriggeredNotification() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
const PrefService::Preference* last_sent =
local_state_->FindPreference(kTipsNotificationsLastSent);
if (last_sent->IsDefaultValue()) {
return;
}
TipsNotificationType type =
static_cast<TipsNotificationType>(last_sent->GetValue()->GetInt());
const char* triggered_histogram =
CanSendReactivation() ? "IOS.Notifications.Tips.Proactive.Triggered"
: "IOS.Notifications.Tips.Triggered";
base::UmaHistogramEnumeration(triggered_histogram, type);
base::UmaHistogramEnumeration("IOS.Notification.Received",
NotificationTypeForTipsNotificationType(type));
local_state_->SetInteger(kTipsNotificationsLastTriggered, int(type));
local_state_->ClearPref(kTipsNotificationsLastSent);
}
void TipsNotificationClient::MaybeLogDismissedNotification() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (interacted_type_.has_value()) {
local_state_->ClearPref(kTipsNotificationsLastTriggered);
return;
}
const PrefService::Preference* last_triggered =
local_state_->FindPreference(kTipsNotificationsLastTriggered);
if (last_triggered->IsDefaultValue()) {
return;
}
auto completion = base::CallbackToBlock(base::BindPostTask(
base::SequencedTaskRunner::GetCurrentDefault(),
base::BindOnce(&TipsNotificationClient::OnGetDeliveredNotifications,
weak_ptr_factory_.GetWeakPtr())));
[UNUserNotificationCenter.currentNotificationCenter
getDeliveredNotificationsWithCompletionHandler:completion];
}
void TipsNotificationClient::OnGetDeliveredNotifications(
NSArray<UNNotification*>* notifications) {
for (UNNotification* notification in notifications) {
if ([notification.request.identifier isEqualToString:kTipsNotificationId]) {
return;
}
}
// No notification was found, so it must have been dismissed.
int dismiss_count =
local_state_->GetInteger(kTipsNotificationsDismissCount) + 1;
local_state_->SetInteger(kTipsNotificationsDismissCount, dismiss_count);
TipsNotificationType type = static_cast<TipsNotificationType>(
local_state_->GetInteger(kTipsNotificationsLastTriggered));
const char* dismissed_histogram =
CanSendReactivation() ? "IOS.Notifications.Tips.Proactive.Dismissed"
: "IOS.Notifications.Tips.Dismissed";
base::UmaHistogramEnumeration(dismissed_histogram, type);
local_state_->ClearPref(kTipsNotificationsLastTriggered);
}
bool TipsNotificationClient::IsPermitted() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// TODO(crbug.com/325279788): use
// GetMobileNotificationPermissionStatusForClient to determine opt-in
// state.
return local_state_->GetDict(prefs::kAppLevelPushNotificationPermissions)
.FindBool(kTipsNotificationKey)
.value_or(false);
}
bool TipsNotificationClient::CanSendReactivation() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// If the user has opted-in for Tips, or First-Run was more than 4 weeks ago,
// or if the feature is not enabled, Reactivation notifications should not
// be sent.
if (permitted_ || !IsFirstRunRecent(base::Days(28)) ||
!IsIOSReactivationNotificationsEnabled()) {
return false;
}
UNAuthorizationStatus auth_status =
[PushNotificationUtil getSavedPermissionSettings];
if (auth_status != UNAuthorizationStatusProvisional) {
return false;
}
return local_state_->GetInteger(kReactivationNotificationsCanceledCount) < 2;
}
bool TipsNotificationClient::DismissLimitReached() {
int dismiss_limit = TipsNotificationsDismissLimit();
if (!dismiss_limit) {
return false;
}
int dismiss_count = GetApplicationContext()->GetLocalState()->GetInteger(
kTipsNotificationsDismissCount);
return dismiss_count >= dismiss_limit;
}
void TipsNotificationClient::OnPermittedPrefChanged(const std::string& name) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
bool newpermitted_ = IsPermitted();
if (permitted_ != newpermitted_) {
ClearAllRequestedNotifications();
if (IsSceneLevelForegroundActive()) {
CheckAndMaybeRequestNotification(base::DoNothing());
}
}
}
void TipsNotificationClient::OnAuthPrefChanged(const std::string& name) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
UNAuthorizationStatus auth_status =
[PushNotificationUtil getSavedPermissionSettings];
if (IsSceneLevelForegroundActive() &&
auth_status == UNAuthorizationStatusProvisional) {
CheckAndMaybeRequestNotification(base::DoNothing());
}
}
void TipsNotificationClient::ClassifyUser() {
if (!local_state_->GetUserPrefValue(kTipsNotificationsLastRequestedTime)) {
return;
}
base::Time now = base::Time::Now();
base::Time last_request =
local_state_->GetTime(kTipsNotificationsLastRequestedTime);
if (now < last_request + base::Hours(2)) {
// Not enough time has passed to classify the user.
return;
}
if (now > last_request + TipsNotificationTriggerDelta(
CanSendReactivation(),
TipsNotificationUserType::kUnknown)) {
user_type_ = TipsNotificationUserType::kLessEngaged;
} else {
user_type_ = TipsNotificationUserType::kActiveSeeker;
}
SetUserType(local_state_, user_type_);
}
bool TipsNotificationClient::HasIdentitiesOnDevice(ProfileIOS* profile) const {
return !IdentityManagerFactory::GetForProfile(profile)
->GetAccountsOnDevice()
.empty();
}