blob: bafda764d8a25da211dbb661e1edc70187ed0020 [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/signin/public/identity_manager/identity_manager.h"
#import "ios/chrome/browser/feature_engagement/model/tracker_factory.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/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/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/tips_notifications/model/tips_notification_criteria.h"
#import "ios/chrome/browser/tips_notifications/model/tips_notification_presenter.h"
#import "ios/chrome/browser/tips_notifications/model/utils.h"
namespace {
// The amount of time used to determine if the user should be classified.
const base::TimeDelta kClassifyUserRecency = base::Hours(2);
// The trigger time used for the one time default browser notification.
const base::TimeDelta OneTimeNotificationTriggerDelta = base::Hours(24);
// 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 `time` is less time ago than `delta`.
bool IsRecent(base::Time time, base::TimeDelta delta) {
return base::Time::Now() - time < delta;
}
} // 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_ = GetTipsNotificationUserType(local_state_);
}
TipsNotificationClient::~TipsNotificationClient() = default;
bool TipsNotificationClient::CanHandleNotification(
UNNotification* notification) {
return IsTipsNotification(notification.request);
}
std::optional<NotificationType> TipsNotificationClient::GetNotificationType(
UNNotification* notification) {
if (!CanHandleNotification(notification)) {
return std::nullopt;
}
std::optional<TipsNotificationType> tips_type =
ParseTipsNotificationType(notification.request);
if (!tips_type) {
return std::nullopt;
}
return NotificationTypeForTipsNotificationType(tips_type.value());
}
bool TipsNotificationClient::HandleNotificationInteraction(
UNNotificationResponse* response) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!CanHandleNotification(response.notification)) {
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 = GetActiveForegroundBrowser();
CHECK(browser);
TipsNotificationPresenter::Present(browser->AsWeakPtr(), type);
// If a relevant 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_);
UpdateProvisionalAllowed();
forced_type_ = ForcedTipsNotificationType();
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_);
// Check for the one-time default browser notification.
if (base::FeatureList::IsEnabled(kIOSOneTimeDefaultBrowserNotification)) {
ProfileIOS* profile = GetActiveForegroundProfile();
if (profile) {
TipsNotificationType type = TipsNotificationType::kDefaultBrowser;
std::unique_ptr<TipsNotificationCriteria> criteria =
std::make_unique<TipsNotificationCriteria>(profile, local_state_,
CanSendReactivation());
if (criteria->ShouldSendNotification(type)) {
one_time_type_ = type;
}
}
}
if (!request) {
MaybeLogTriggeredNotification();
MaybeLogDismissedNotification();
interacted_type_ = std::nullopt;
MaybeRequestNotification(base::DoNothing());
return;
}
MaybeLogDismissedNotification();
interacted_type_ = std::nullopt;
std::optional<TipsNotificationType> type = ParseTipsNotificationType(request);
if (CanSendReactivation()) {
ClearAllRequestedNotifications();
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());
}
if (one_time_type_.has_value()) {
// If a pending request is found, clear it to prioritize the one-time
// notification.
ClearAllRequestedNotifications();
if (type.has_value()) {
MarkNotificationTypeNotSent(type.value());
}
MaybeRequestNotification(base::DoNothing());
}
}
void TipsNotificationClient::MaybeRequestNotification(
base::OnceClosure completion) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if ((!permitted_ && !CanSendReactivation())) {
std::move(completion).Run();
return;
}
ProfileIOS* profile = GetActiveForegroundProfile();
if (!profile) {
std::move(completion).Run();
return;
}
if (forced_type_.has_value()) {
RequestNotification(forced_type_.value(), profile->GetProfileName(),
std::move(completion));
return;
}
if (one_time_type_.has_value()) {
if (one_time_type_ == TipsNotificationType::kDefaultBrowser) {
// The FET's feature should be triggered.
feature_engagement::Tracker* tracker =
feature_engagement::TrackerFactory::GetForProfile(profile);
if (tracker->ShouldTriggerHelpUI(
feature_engagement::
kIPHiOSOneTimeDefaultBrowserNotificationFeature)) {
RequestNotification(one_time_type_.value(), profile->GetProfileName(),
std::move(completion));
tracker->Dismissed(feature_engagement::
kIPHiOSOneTimeDefaultBrowserNotificationFeature);
tracker->NotifyEvent("default_browser_promos_group_trigger");
}
}
one_time_type_ = std::nullopt;
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());
std::unique_ptr<TipsNotificationCriteria> criteria =
std::make_unique<TipsNotificationCriteria>(profile, local_state_,
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 (criteria->ShouldSendNotification(type)) {
RequestNotification(type, profile->GetProfileName(),
std::move(completion));
return;
}
}
std::move(completion).Run();
}
void TipsNotificationClient::ClearAllRequestedNotifications() {
[UNUserNotificationCenter.currentNotificationCenter
removePendingNotificationRequestsWithIdentifiers:@[
kTipsNotificationId
]];
}
void TipsNotificationClient::RequestNotification(
TipsNotificationType notification_type,
std::string_view profile_name,
base::OnceClosure completion) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
base::TimeDelta trigger_delta = TipsNotificationTriggerDelta(
CanSendReactivation(), user_type_, notification_type);
if (one_time_type_.has_value()) {
trigger_delta = std::min(trigger_delta, OneTimeNotificationTriggerDelta);
}
if (IsNotificationCollisionManagementEnabled()) {
ScheduledNotificationRequest request = {
kTipsNotificationId,
ContentForTipsNotificationType(notification_type, CanSendReactivation(),
profile_name),
trigger_delta};
CheckRateLimitBeforeSchedulingNotification(
request,
base::BindPostTask(
base::SequencedTaskRunner::GetCurrentDefault(),
base::BindOnce(&TipsNotificationClient::OnNotificationRequested,
weak_ptr_factory_.GetWeakPtr(), notification_type)
.Then(std::move(completion))));
MarkNotificationTypeSent(notification_type);
return;
}
UNNotificationRequest* request = [UNNotificationRequest
requestWithIdentifier:kTipsNotificationId
content:ContentForTipsNotificationType(
notification_type, CanSendReactivation(),
profile_name)
trigger:[UNTimeIntervalNotificationTrigger
triggerWithTimeInterval:trigger_delta
.InSecondsF()
repeats:NO]];
auto completion_block = base::CallbackToBlock(base::BindPostTask(
base::SequencedTaskRunner::GetCurrentDefault(),
base::BindOnce(&TipsNotificationClient::OnNotificationRequested,
weak_ptr_factory_.GetWeakPtr(), notification_type)
.Then(std::move(completion))));
[UNUserNotificationCenter.currentNotificationCenter
addNotificationRequest:request
withCompletionHandler:completion_block];
MarkNotificationTypeSent(notification_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::IsSceneLevelForegroundActive() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return GetActiveForegroundBrowser() != nullptr;
}
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);
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() const {
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() const {
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() || !provisional_allowed_) {
return false;
}
UNAuthorizationStatus auth_status =
[PushNotificationUtil getSavedPermissionSettings];
if (auth_status != UNAuthorizationStatusProvisional) {
return false;
}
return local_state_->GetInteger(kReactivationNotificationsCanceledCount) <
2 ||
forced_type_.has_value();
}
void TipsNotificationClient::UpdateProvisionalAllowed() {
Browser* browser = GetActiveForegroundBrowser();
CHECK(browser);
provisional_allowed_ = [PushNotificationUtil
provisionalAllowedByPolicyForProfile:browser->GetProfile()];
}
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 last_request =
local_state_->GetTime(kTipsNotificationsLastRequestedTime);
if (IsRecent(last_request, kClassifyUserRecency)) {
// Not enough time has passed to classify the user.
return;
}
base::TimeDelta trigger_delta = TipsNotificationTriggerDelta(
CanSendReactivation(), TipsNotificationUserType::kUnknown);
if (IsRecent(last_request, trigger_delta)) {
user_type_ = TipsNotificationUserType::kActiveSeeker;
} else {
user_type_ = TipsNotificationUserType::kLessEngaged;
}
SetTipsNotificationUserType(local_state_, user_type_);
base::UmaHistogramEnumeration("IOS.Notifications.Tips.UserType", user_type_);
}
ProfileIOS* TipsNotificationClient::GetActiveForegroundProfile() const {
Browser* browser = GetActiveForegroundBrowser();
if (!browser) {
return nullptr;
}
return browser->GetProfile();
}