// 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/safety_check_notifications/model/safety_check_notification_client.h"
#import "base/functional/bind.h"
#import "base/functional/callback_helpers.h"
#import "base/location.h"
#import "base/task/bind_post_task.h"
#import "components/prefs/pref_service.h"
#import "ios/chrome/browser/push_notification/model/constants.h"
#import "ios/chrome/browser/push_notification/model/push_notification_client_id.h"
#import "ios/chrome/browser/safety_check/model/ios_chrome_safety_check_manager.h"
#import "ios/chrome/browser/safety_check/model/ios_chrome_safety_check_manager_factory.h"
#import "ios/chrome/browser/safety_check_notifications/utils/constants.h"
#import "ios/chrome/browser/safety_check_notifications/utils/utils.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_state/chrome_browser_state.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
namespace {
// Returns all notifications from `requests` matching `identifiers`, if any.
NSArray<UNNotificationRequest*>* NotificationsWithIdentifiers(
NSSet<NSString*>* identifiers,
NSArray<UNNotificationRequest*>* requests) {
NSMutableArray<UNNotificationRequest*>* matching_requests =
[NSMutableArray array];
for (UNNotificationRequest* request in requests) {
if ([identifiers containsObject:request.identifier]) {
[matching_requests addObject:request];
}
}
return matching_requests;
}
} // namespace
SafetyCheckNotificationClient::SafetyCheckNotificationClient(
const scoped_refptr<base::SequencedTaskRunner> task_runner)
: PushNotificationClient(PushNotificationClientId::kSafetyCheck),
task_runner_(task_runner) {
CHECK(task_runner);
}
SafetyCheckNotificationClient::~SafetyCheckNotificationClient() = default;
void SafetyCheckNotificationClient::HandleNotificationInteraction(
UNNotificationResponse* response) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// TODO(crbug.com/356624000): Implement `HandleNotificationInteraction()` to
// process user interactions with notifications (e.g., taps, dismissals).
}
UIBackgroundFetchResult
SafetyCheckNotificationClient::HandleNotificationReception(
NSDictionary<NSString*, id>* notification) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// TODO(crbug.com/356626805): Implement `HandleNotificationReception()` to log
// notification receipt for analytics/debugging.
return UIBackgroundFetchResultNoData;
}
NSArray<UNNotificationCategory*>*
SafetyCheckNotificationClient::RegisterActionableNotifications() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// TODO(crbug.com/356624328): Implement actionable Safety Check notifications
// allowing users to take direct actions from the notification.
return @[];
}
void SafetyCheckNotificationClient::OnSceneActiveForegroundBrowserReady() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
OnSceneActiveForegroundBrowserReady(base::DoNothing());
}
void SafetyCheckNotificationClient::OnSceneActiveForegroundBrowserReady(
base::OnceClosure completion) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// TODO(crbug.com/362479882): Exit if the user shouldn't receive a new Safety
// Check notification (e.g., notifications disabled, recent notification
// already shown).
if (!IsPermitted()) {
std::move(completion).Run();
return;
}
// Confirm that `SafetyCheckNotificationClient` is not observing
// `IOSChromeSafetyCheckManager` before registering itself as an observer for
// Safety Check updates.
if (!IOSChromeSafetyCheckManagerObserver::IsInObserverList()) {
Browser* browser = GetSceneLevelForegroundActiveBrowser();
if (!browser) {
std::move(completion).Run();
return;
}
ChromeBrowserState* browser_state = browser->GetBrowserState();
IOSChromeSafetyCheckManager* safety_check_manager =
IOSChromeSafetyCheckManagerFactory::GetForBrowserState(browser_state);
safety_check_manager->AddObserver(this);
update_chrome_check_state_ =
safety_check_manager->GetUpdateChromeCheckState();
safe_browsing_check_state_ =
safety_check_manager->GetSafeBrowsingCheckState();
password_check_state_ = safety_check_manager->GetPasswordCheckState();
insecure_password_counts_ =
safety_check_manager->GetInsecurePasswordCounts();
ClearAndRescheduleSafetyCheckNotifications(
update_chrome_check_state_, safe_browsing_check_state_,
password_check_state_, insecure_password_counts_,
std::move(completion));
return;
}
// TODO(crbug.com/347975105): Implement
// `OnSceneActiveForegroundBrowserReady()` to conditionally schedule
// notifications.
std::move(completion).Run();
}
void SafetyCheckNotificationClient::PasswordCheckStateChanged(
PasswordSafetyCheckState state,
password_manager::InsecurePasswordCounts insecure_password_counts) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Avoid modifying notifications while the Password check is running.
// Wait for a meaningful state change that influences whether Password
// notifications should be removed or scheduled.
if (state == PasswordSafetyCheckState::kRunning) {
return;
}
password_check_state_ = state;
insecure_password_counts_ = insecure_password_counts;
ClearAndRescheduleSafetyCheckNotifications(
update_chrome_check_state_, safe_browsing_check_state_,
password_check_state_, insecure_password_counts_, base::DoNothing());
}
void SafetyCheckNotificationClient::SafeBrowsingCheckStateChanged(
SafeBrowsingSafetyCheckState state) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Avoid modifying notifications while the Safe Browsing check is running.
// Wait for a meaningful state change that influences whether Safe Browsing
// notifications should be removed or scheduled.
if (state == SafeBrowsingSafetyCheckState::kRunning) {
return;
}
safe_browsing_check_state_ = state;
ClearAndRescheduleSafetyCheckNotifications(
update_chrome_check_state_, safe_browsing_check_state_,
password_check_state_, insecure_password_counts_, base::DoNothing());
}
void SafetyCheckNotificationClient::UpdateChromeCheckStateChanged(
UpdateChromeSafetyCheckState state) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Avoid modifying notifications while the Update Chrome check is running.
// Wait for a meaningful state change that influences whether Update Chrome
// notifications should be removed or scheduled.
if (state == UpdateChromeSafetyCheckState::kRunning) {
return;
}
update_chrome_check_state_ = state;
ClearAndRescheduleSafetyCheckNotifications(
update_chrome_check_state_, safe_browsing_check_state_,
password_check_state_, insecure_password_counts_, base::DoNothing());
}
void SafetyCheckNotificationClient::RunningStateChanged(
RunningSafetyCheckState state) {
// Do nothing. This method is currently a no-op as the running state of Safety
// Check does not directly impact notification scheduling or removal.
}
void SafetyCheckNotificationClient::ManagerWillShutdown(
IOSChromeSafetyCheckManager* safety_check_manager) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
safety_check_manager->RemoveObserver(this);
}
void SafetyCheckNotificationClient::GetPendingRequests(
NSArray<NSString*>* identifiers,
GetPendingRequestsCallback completion) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto callback = base::CallbackToBlock(base::BindPostTask(
task_runner_, base::BindOnce(&NotificationsWithIdentifiers,
[NSSet setWithArray:identifiers])
.Then(std::move(completion))));
[UNUserNotificationCenter.currentNotificationCenter
getPendingNotificationRequestsWithCompletionHandler:callback];
}
bool SafetyCheckNotificationClient::IsPermitted() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// TODO(crbug.com/362260014): Replace current opt-in state logic with
// `GetMobileNotificationPermissionStatusForClient()` once
// `PushNotificationClient` dependencies are refactored.
PrefService* local_pref_service = GetApplicationContext()->GetLocalState();
return local_pref_service
->GetDict(prefs::kAppLevelPushNotificationPermissions)
.FindBool(kSafetyCheckNotificationKey)
.value_or(false);
}
void SafetyCheckNotificationClient::OnNotificationsCleared(
NSArray<NSString*>* identifiers,
NSArray<UNNotificationRequest*>* requests) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (![requests count]) {
// TODO(crbug.com/362481419): Add logging to track the state of the
// notification (requested, triggered, etc.).
return;
}
[UNUserNotificationCenter.currentNotificationCenter
removePendingNotificationRequestsWithIdentifiers:identifiers];
}
void SafetyCheckNotificationClient::ClearNotifications(
NSArray<NSString*>* identifiers,
base::OnceClosure completion) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
GetPendingRequests(
identifiers,
base::BindOnce(&SafetyCheckNotificationClient::OnNotificationsCleared,
weak_ptr_factory_.GetWeakPtr(), identifiers)
.Then(std::move(completion)));
}
void SafetyCheckNotificationClient::ScheduleSafetyCheckNotifications(
UpdateChromeSafetyCheckState update_chrome_state,
SafeBrowsingSafetyCheckState safe_browsing_state,
PasswordSafetyCheckState password_state,
password_manager::InsecurePasswordCounts insecure_password_counts,
base::OnceClosure completion) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// TODO(crbug.com/362479882): Exit if the user shouldn't receive a new Safety
// Check notification (e.g., notifications disabled, recent notification
// already shown).
if (!IsPermitted()) {
std::move(completion).Run();
return;
}
// TODO(crbug.com/362481419): Add completion handler to log metrics and
// actions when Safety Check notifications are requested.
// TODO(crbug.com/362479882): Use experimental arm to determine if single or
// multiple notifications are allowed.
UNNotificationRequest* safe_browsing_notification =
SafeBrowsingNotificationRequest(safe_browsing_state);
if (safe_browsing_notification) {
[UNUserNotificationCenter.currentNotificationCenter
addNotificationRequest:safe_browsing_notification
withCompletionHandler:nil];
}
UNNotificationRequest* update_chrome_notification =
UpdateChromeNotificationRequest(update_chrome_state);
if (update_chrome_notification) {
[UNUserNotificationCenter.currentNotificationCenter
addNotificationRequest:update_chrome_notification
withCompletionHandler:nil];
}
UNNotificationRequest* password_notification =
PasswordNotificationRequest(password_state, insecure_password_counts);
if (password_notification) {
[UNUserNotificationCenter.currentNotificationCenter
addNotificationRequest:password_notification
withCompletionHandler:nil];
}
std::move(completion).Run();
}
void SafetyCheckNotificationClient::ClearAndRescheduleSafetyCheckNotifications(
UpdateChromeSafetyCheckState update_chrome_state,
SafeBrowsingSafetyCheckState safe_browsing_state,
PasswordSafetyCheckState password_state,
password_manager::InsecurePasswordCounts insecure_password_counts,
base::OnceClosure completion) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
ClearNotifications(
@[
kSafetyCheckSafeBrowsingNotificationID,
kSafetyCheckUpdateChromeNotificationID,
kSafetyCheckPasswordNotificationID,
],
base::BindOnce(
&SafetyCheckNotificationClient::ScheduleSafetyCheckNotifications,
weak_ptr_factory_.GetWeakPtr(), update_chrome_state,
safe_browsing_state, password_state, insecure_password_counts,
std::move(completion)));
}