blob: c41beaa0e464af513ffc384c5ac60ff687e63154 [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/reminder_notifications/model/reminder_notification_client.h"
#import <optional>
#import "base/check.h"
#import "base/functional/bind.h"
#import "base/functional/callback_helpers.h"
#import "base/json/values_util.h"
#import "base/strings/sys_string_conversions.h"
#import "base/task/bind_post_task.h"
#import "base/task/sequenced_task_runner.h"
#import "base/values.h"
#import "components/prefs/pref_change_registrar.h"
#import "components/prefs/pref_service.h"
#import "components/prefs/scoped_user_pref_update.h"
#import "ios/chrome/browser/push_notification/model/constants.h"
#import "ios/chrome/browser/reminder_notifications/coordinator/reminder_notifications_mediator.h"
#import "ios/chrome/browser/reminder_notifications/model/reminder_notification_builder.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/web/public/web_state.h"
#import "url/gurl.h"
ReminderNotificationClient::ReminderNotificationClient(ProfileIOS* profile)
: PushNotificationClient(PushNotificationClientId::kReminders, profile) {
CHECK(profile);
pref_change_registrar_ = std::make_unique<PrefChangeRegistrar>();
PrefService* prefs = profile->GetPrefs();
pref_change_registrar_->Init(prefs);
pref_change_registrar_->Add(
prefs::kReminderNotifications,
base::BindRepeating(
&ReminderNotificationClient::OnReminderDataPrefChanged,
weak_ptr_factory_.GetWeakPtr()));
pref_change_registrar_->Add(
prefs::kFeaturePushNotificationPermissions,
base::BindRepeating(&ReminderNotificationClient::OnPermissionsPrefChanged,
weak_ptr_factory_.GetWeakPtr()));
}
ReminderNotificationClient::~ReminderNotificationClient() = default;
std::optional<NotificationType> ReminderNotificationClient::GetNotificationType(
UNNotification* notification) {
if (CanHandleNotification(notification)) {
return NotificationType::kReminder;
}
return std::nullopt;
}
bool ReminderNotificationClient::CanHandleNotification(
UNNotification* notification) {
return [notification.request.identifier
hasPrefix:kReminderNotificationsIdentifierPrefix];
}
bool ReminderNotificationClient::HandleNotificationInteraction(
UNNotificationResponse* response) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!CanHandleNotification(response.notification)) {
return false;
}
NSDictionary* user_info = response.notification.request.content.userInfo;
NSString* url_string = user_info[@"url"];
if (!url_string || url_string.length == 0) {
// TODO(crbug.com/390432325): Consider adding UMA logs for missing URL.
return false;
}
GURL url(base::SysNSStringToUTF8(url_string));
if (!url.is_valid()) {
// TODO(crbug.com/390432325): Consider adding UMA logs for invalid URL.
return false;
}
// TODO(crbug.com/422449238): Consider adding UMA logs for interaction
// handling.
LoadUrlInNewTab(url);
return true;
}
std::optional<UIBackgroundFetchResult>
ReminderNotificationClient::HandleNotificationReception(
NSDictionary<NSString*, id>* userInfo) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return std::nullopt;
}
NSArray<UNNotificationCategory*>*
ReminderNotificationClient::RegisterActionableNotifications() {
return @[];
}
bool ReminderNotificationClient::IsPermitted() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
ProfileIOS* current_profile = GetProfile();
return current_profile->GetPrefs()
->GetDict(prefs::kFeaturePushNotificationPermissions)
.FindBool(kReminderNotificationKey)
.value_or(false);
}
void ReminderNotificationClient::OnReminderDataPrefChanged() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
ScheduleNewReminders();
}
void ReminderNotificationClient::OnPermissionsPrefChanged() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!IsPermitted()) {
CancelAllNotifications(base::DoNothing());
return;
}
ScheduleNewReminders();
}
void ReminderNotificationClient::CancelAllNotifications(
base::OnceClosure completion_handler) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto completion_block = base::CallbackToBlock(base::BindPostTask(
base::SequencedTaskRunner::GetCurrentDefault(),
base::BindOnce(
&ReminderNotificationClient::OnGetPendingNotificationsForCancellation,
weak_ptr_factory_.GetWeakPtr(), std::move(completion_handler))));
[[UNUserNotificationCenter currentNotificationCenter]
getPendingNotificationRequestsWithCompletionHandler:completion_block];
}
void ReminderNotificationClient::OnGetPendingNotificationsForCancellation(
base::OnceClosure completion_handler,
NSArray<UNNotificationRequest*>* requests) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
NSMutableArray<NSString*>* identifiers_to_remove = [NSMutableArray array];
for (UNNotificationRequest* request in requests) {
if ([request.identifier hasPrefix:kReminderNotificationsIdentifierPrefix]) {
[identifiers_to_remove addObject:request.identifier];
}
}
if (identifiers_to_remove.count > 0) {
[[UNUserNotificationCenter currentNotificationCenter]
removePendingNotificationRequestsWithIdentifiers:identifiers_to_remove];
}
std::move(completion_handler).Run();
}
void ReminderNotificationClient::ScheduleNotification(
const GURL& reminder_url,
const base::Value::Dict& reminder_details,
std::string_view profile_name) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(!profile_name.empty());
CHECK(reminder_url.is_valid());
const base::Value* reminder_time_value =
reminder_details.Find(kReminderNotificationsTimeKey);
if (!reminder_time_value) {
// TODO(crbug.com/422449238): Consider adding UMA/logging for this failure
// case.
return;
}
std::optional<base::Time> reminder_time =
base::ValueToTime(reminder_time_value);
if (!reminder_time.has_value()) {
// TODO(crbug.com/422449238): Consider adding UMA/logging for this failure
// case.
return;
}
ReminderNotificationBuilder* builder =
[[ReminderNotificationBuilder alloc] initWithURL:reminder_url
time:reminder_time.value()];
Browser* browser = GetActiveForegroundBrowser();
web::WebState* web_state =
browser ? browser->GetWebStateList()->GetActiveWebState() : nullptr;
if (web_state &&
(web_state->GetLastCommittedURL() == reminder_url ||
web_state->GetVisibleURL() == reminder_url) &&
!web_state->GetTitle().empty()) {
[builder setPageTitle:base::SysUTF16ToNSString(web_state->GetTitle())];
}
// TODO(crbug.com/392921766): Set page image for the notification.
ScheduledNotificationRequest request = [builder buildRequest];
ScheduleProfileNotification(
request,
base::BindOnce(&ReminderNotificationClient::OnNotificationScheduled,
weak_ptr_factory_.GetWeakPtr(), reminder_url),
profile_name);
}
void ReminderNotificationClient::OnNotificationScheduled(
const GURL& scheduled_url,
NSError* error) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (error) {
// TODO(crbug.com/422449238): Consider adding UMA for scheduling failures.
return;
}
ProfileIOS* current_profile = GetProfile();
if (!current_profile) {
// Profile might have been destroyed before the callback ran.
// TODO(crbug.com/422449238): Consider adding UMA for scheduling failures.
return;
}
PrefService* prefs = current_profile->GetPrefs();
ScopedDictPrefUpdate update(prefs, prefs::kReminderNotifications);
update->Remove(scheduled_url.spec());
// TODO(crbug.com/422449238): Consider adding UMA for successful removal.
}
void ReminderNotificationClient::ScheduleNewReminders() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!IsPermitted()) {
return;
}
auto completion_block = base::CallbackToBlock(base::BindPostTask(
base::SequencedTaskRunner::GetCurrentDefault(),
base::BindOnce(&ReminderNotificationClient::ScheduleNewRemindersIfNeeded,
weak_ptr_factory_.GetWeakPtr())));
[[UNUserNotificationCenter currentNotificationCenter]
getPendingNotificationRequestsWithCompletionHandler:completion_block];
}
void ReminderNotificationClient::ScheduleNewRemindersIfNeeded(
NSArray<UNNotificationRequest*>* pending_requests) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
ProfileIOS* current_profile = GetProfile();
if (!current_profile || !IsPermitted()) {
return;
}
PrefService* prefs = current_profile->GetPrefs();
const base::Value::Dict& reminders_in_prefs =
prefs->GetDict(prefs::kReminderNotifications);
if (reminders_in_prefs.empty()) {
return;
}
// Build a set of pending URLs for quick lookup.
std::set<std::string> pending_urls;
for (UNNotificationRequest* request in pending_requests) {
if (![request.identifier
hasPrefix:kReminderNotificationsIdentifierPrefix]) {
continue;
}
NSString* url = request.content.userInfo[@"url"];
pending_urls.insert(base::SysNSStringToUTF8(url));
}
// Iterate through reminders in prefs and schedule notifications only for URLs
// that exist in prefs but not in the notification center.
for (const auto [key, value] : reminders_in_prefs) {
GURL url(key);
if (!url.is_valid()) {
continue;
}
std::string url_string = url.spec();
if (pending_urls.find(url_string) == pending_urls.end()) {
const base::Value::Dict* details = value.GetIfDict();
if (details) {
ScheduleNotification(url, *details, current_profile->GetProfileName());
}
}
}
}