chromium/ios/chrome/browser/safety_check_notifications/model/safety_check_notification_client.mm

// 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)));
}