chromium/ios/chrome/browser/ui/content_suggestions/safety_check/safety_check_magic_stack_mediator.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/ui/content_suggestions/safety_check/safety_check_magic_stack_mediator.h"

#import <optional>

#import "base/memory/raw_ptr.h"
#import "components/pref_registry/pref_registry_syncable.h"
#import "components/prefs/ios/pref_observer_bridge.h"
#import "components/prefs/pref_service.h"
#import "ios/chrome/app/application_delegate/app_state.h"
#import "ios/chrome/app/application_delegate/app_state_observer.h"
#import "ios/chrome/browser/passwords/model/password_checkup_utils.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_constants.h"
#import "ios/chrome/browser/safety_check/model/ios_chrome_safety_check_manager_observer_bridge.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/public/features/system_flags.h"
#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_consumer.h"
#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_view_controller_audience.h"
#import "ios/chrome/browser/ui/content_suggestions/safety_check/safety_check_audience.h"
#import "ios/chrome/browser/ui/content_suggestions/safety_check/safety_check_consumer_source.h"
#import "ios/chrome/browser/ui/content_suggestions/safety_check/safety_check_magic_stack_consumer.h"
#import "ios/chrome/browser/ui/content_suggestions/safety_check/safety_check_prefs.h"
#import "ios/chrome/browser/ui/content_suggestions/safety_check/safety_check_state.h"
#import "ios/chrome/browser/ui/content_suggestions/safety_check/utils.h"

@interface SafetyCheckMagicStackMediator () <AppStateObserver,
                                             PrefObserverDelegate,
                                             SafetyCheckAudience,
                                             SafetyCheckConsumerSource,
                                             SafetyCheckManagerObserver>
@end

@implementation SafetyCheckMagicStackMediator {
  IOSChromeSafetyCheckManager* _safetyCheckManager;
  // Observer for Safety Check changes.
  std::unique_ptr<SafetyCheckObserverBridge> _safetyCheckManagerObserver;
  // Bridge to listen to pref changes.
  std::unique_ptr<PrefObserverBridge> _prefObserverBridge;
  // Registrar for local pref changes notifications.
  PrefChangeRegistrar _prefChangeRegistrar;
  // Registrar for user pref changes notifications.
  PrefChangeRegistrar _userPrefChangeRegistrar;
  // Local State prefs.
  raw_ptr<PrefService> _localState;
  // User prefs.
  raw_ptr<PrefService> _userState;
  AppState* _appState;
  // Used by the Safety Check (Magic Stack) module for the current Safety Check
  // state.
  SafetyCheckState* _safetyCheckState;
  id<SafetyCheckMagicStackConsumer> _safetyCheckConsumer;
}

- (instancetype)initWithSafetyCheckManager:
                    (IOSChromeSafetyCheckManager*)safetyCheckManager
                                localState:(PrefService*)localState
                                 userState:(PrefService*)userState
                                  appState:(AppState*)appState {
  self = [super init];
  if (self) {
    _safetyCheckManager = safetyCheckManager;
    _localState = localState;
    _userState = userState;
    _appState = appState;

    [_appState addObserver:self];

    if (!safety_check_prefs::IsSafetyCheckInMagicStackDisabled(
            IsHomeCustomizationEnabled() ? _userState : _localState)) {
      if (!_prefObserverBridge) {
        _prefObserverBridge = std::make_unique<PrefObserverBridge>(self);
      }

      _prefChangeRegistrar.Init(localState);

      // TODO(crbug.com/40930653): Stop observing
      // `kIosSettingsSafetyCheckLastRunTime` changes once the Settings Safety
      // Check is refactored to use the new Safety Check Manager.
      _prefObserverBridge->ObserveChangesForPreference(
          prefs::kIosSettingsSafetyCheckLastRunTime, &_prefChangeRegistrar);

      _prefObserverBridge->ObserveChangesForPreference(
          prefs::kIosSafetyCheckManagerSafeBrowsingCheckResult,
          &_prefChangeRegistrar);
      _prefObserverBridge->ObserveChangesForPreference(
          safety_check_prefs::kSafetyCheckInMagicStackDisabledPref,
          &_prefChangeRegistrar);

      if (IsHomeCustomizationEnabled()) {
        _userPrefChangeRegistrar.Init(userState);
        _prefObserverBridge->ObserveChangesForPreference(
            prefs::kHomeCustomizationMagicStackSafetyCheckEnabled,
            &_userPrefChangeRegistrar);
      }

      _safetyCheckState = [self initialSafetyCheckState];

      if (ShouldHideSafetyCheckModuleIfNoIssues()) {
        [self updateIssueCount:[_safetyCheckState numberOfIssues]
               withPrefService:localState];
      }

      _safetyCheckManagerObserver =
          std::make_unique<SafetyCheckObserverBridge>(self, safetyCheckManager);

      if (_appState.initStage > InitStageNormalUI &&
          _appState.firstSceneHasInitializedUI &&
          _safetyCheckState.runningState == RunningSafetyCheckState::kRunning) {
        // When the Safety Check Notifications feature is enabled, the Magic
        // Stack should never initiate a Safety Check run.
        //
        // TODO(crbug.com/354727175): Remove `StartSafetyCheck()` from the Magic
        // Stack once Safety Check Notifications fully launches.
        if (!IsSafetyCheckNotificationsEnabled()) {
          safetyCheckManager->StartSafetyCheck();
        }
      }
    }
  }
  return self;
}

- (void)disconnect {
  _safetyCheckManagerObserver.reset();
  if (_prefObserverBridge) {
    _prefChangeRegistrar.RemoveAll();
    if (IsHomeCustomizationEnabled()) {
      _userPrefChangeRegistrar.RemoveAll();
    }

    _prefObserverBridge.reset();
  }
  [_appState removeObserver:self];
}

- (SafetyCheckState*)safetyCheckState {
  return _safetyCheckState;
}

- (void)disableModule {
  safety_check_prefs::DisableSafetyCheckInMagicStack(
      IsHomeCustomizationEnabled() ? _userState : _localState);
}

- (void)reset {
  _safetyCheckState = [[SafetyCheckState alloc]
      initWithUpdateChromeState:UpdateChromeSafetyCheckState::kDefault
                  passwordState:PasswordSafetyCheckState::kDefault
              safeBrowsingState:SafeBrowsingSafetyCheckState::kDefault
                   runningState:RunningSafetyCheckState::kDefault];
  _safetyCheckState.audience = self;
  _safetyCheckState.safetyCheckConsumerSource = self;
}

#pragma mark - SafetyCheckConsumerSource

- (void)addConsumer:(id<SafetyCheckMagicStackConsumer>)consumer {
  _safetyCheckConsumer = consumer;
}

#pragma mark - SafetyCheckAudience

// Called when a Safety Check item is selected by the user.
- (void)didSelectSafetyCheckItem:(SafetyCheckItemType)type {
  [self.presentationAudience didSelectSafetyCheckItem:type];
}

#pragma mark - SafetyCheckManagerObserver

- (void)passwordCheckStateChanged:(PasswordSafetyCheckState)state
           insecurePasswordCounts:(password_manager::InsecurePasswordCounts)
                                      insecurePasswordCounts {
  _safetyCheckState.passwordState = state;
  _safetyCheckState.weakPasswordsCount = insecurePasswordCounts.weak_count;
  _safetyCheckState.reusedPasswordsCount = insecurePasswordCounts.reused_count;
  _safetyCheckState.compromisedPasswordsCount =
      insecurePasswordCounts.compromised_count;
}

- (void)safeBrowsingCheckStateChanged:(SafeBrowsingSafetyCheckState)state {
  _safetyCheckState.safeBrowsingState = state;
}

- (void)updateChromeCheckStateChanged:(UpdateChromeSafetyCheckState)state {
  _safetyCheckState.updateChromeState = state;
}

- (void)runningStateChanged:(RunningSafetyCheckState)state {
  _safetyCheckState.runningState = state;
  _safetyCheckState.shouldShowSeeMore = [_safetyCheckState numberOfIssues] > 2;

  if (ShouldHideSafetyCheckModuleIfNoIssues()) {
    [self updateIssueCount:[_safetyCheckState numberOfIssues]
           withPrefService:_localState];
  }

  if (safety_check_prefs::IsSafetyCheckInMagicStackDisabled(
          IsHomeCustomizationEnabled() ? _userState : _localState)) {
    // Safety Check can be disabled by long-pressing the module, so
    // SafetyCheckManager can still be running and returning results even after
    // disabling.
    return;
  }

  // Ensures the consumer gets the latest Safety Check state only when the
  // running state changes; this avoids calling the consumer every time an
  // individual check state changes.
  _safetyCheckState.audience = self;
  [_safetyCheckConsumer safetyCheckStateDidChange:_safetyCheckState];
}

- (void)safetyCheckManagerWillShutdown {
  _safetyCheckManagerObserver.reset();
}

#pragma mark - AppStateObserver

// Conditionally starts the Safety Check if the upcoming init stage is
// `InitStageFinal` and the Safety Check state indicates it's running.
//
// NOTE: It's safe to call `StartSafetyCheck()` multiple times, because calling
// `StartSafetyCheck()` on an already-running Safety Check is a no-op.
- (void)appState:(AppState*)appState
    willTransitionToInitStage:(InitStage)nextInitStage {
  if (!safety_check_prefs::IsSafetyCheckInMagicStackDisabled(
          IsHomeCustomizationEnabled() ? _userState : _localState) &&
      nextInitStage == InitStageFinal && appState.firstSceneHasInitializedUI &&
      _safetyCheckState.runningState == RunningSafetyCheckState::kRunning) {
    // When the Safety Check Notifications feature is enabled, the Magic
    // Stack should never initiate a Safety Check run.
    //
    // TODO(crbug.com/354727175): Remove `StartSafetyCheck()` from the Magic
    // Stack once Safety Check Notifications fully launches.
    if (!IsSafetyCheckNotificationsEnabled()) {
      _safetyCheckManager->StartSafetyCheck();
    }
  }
}

#pragma mark - PrefObserverDelegate

- (void)onPreferenceChanged:(const std::string&)preferenceName {
  if (preferenceName == prefs::kIosSettingsSafetyCheckLastRunTime ||
      preferenceName == prefs::kIosSafetyCheckManagerSafeBrowsingCheckResult) {
    _safetyCheckState.lastRunTime = [self latestSafetyCheckRunTimestamp];

    _safetyCheckState.safeBrowsingState =
        SafeBrowsingSafetyCheckStateForName(
            _localState->GetString(
                prefs::kIosSafetyCheckManagerSafeBrowsingCheckResult))
            .value_or(_safetyCheckState.safeBrowsingState);

    // Trigger a module update when the Last Run Time, or Safe Browsing state,
    // has changed.
    [self runningStateChanged:_safetyCheckState.runningState];
  } else if (preferenceName ==
             safety_check_prefs::kSafetyCheckInMagicStackDisabledPref) {
    if (safety_check_prefs::IsSafetyCheckInMagicStackDisabled(_localState)) {
      [self.delegate removeSafetyCheckModule];
    }
  } else if (preferenceName ==
                 prefs::kHomeCustomizationMagicStackSafetyCheckEnabled &&
             !_userState->GetBoolean(
                 prefs::kHomeCustomizationMagicStackSafetyCheckEnabled)) {
    CHECK(IsHomeCustomizationEnabled());
    [self.delegate removeSafetyCheckModule];
  }
}

#pragma mark - Private

// Creates the initial `SafetyCheckState` based on the previous check states
// stored in Prefs, or (for development builds) the overridden check states via
// Experimental settings.
- (SafetyCheckState*)initialSafetyCheckState {
  SafetyCheckState* state = [[SafetyCheckState alloc]
      initWithUpdateChromeState:UpdateChromeSafetyCheckState::kDefault
                  passwordState:PasswordSafetyCheckState::kDefault
              safeBrowsingState:SafeBrowsingSafetyCheckState::kDefault
                   runningState:RunningSafetyCheckState::kDefault];

  // Update Chrome check.
  std::optional<UpdateChromeSafetyCheckState> overrideUpdateChromeState =
      experimental_flags::GetUpdateChromeSafetyCheckState();

  state.updateChromeState = overrideUpdateChromeState.value_or(
      _safetyCheckManager->GetUpdateChromeCheckState());

  // Password check.
  std::optional<PasswordSafetyCheckState> overridePasswordState =
      experimental_flags::GetPasswordSafetyCheckState();

  state.passwordState = overridePasswordState.value_or(
      _safetyCheckManager->GetPasswordCheckState());

  // Safe Browsing check.
  std::optional<SafeBrowsingSafetyCheckState> overrideSafeBrowsingState =
      experimental_flags::GetSafeBrowsingSafetyCheckState();

  state.safeBrowsingState = overrideSafeBrowsingState.value_or(
      _safetyCheckManager->GetSafeBrowsingCheckState());

  // Insecure credentials.
  std::optional<int> overrideWeakPasswordsCount =
      experimental_flags::GetSafetyCheckWeakPasswordsCount();

  std::optional<int> overrideReusedPasswordsCount =
      experimental_flags::GetSafetyCheckReusedPasswordsCount();

  std::optional<int> overrideCompromisedPasswordsCount =
      experimental_flags::GetSafetyCheckCompromisedPasswordsCount();

  bool passwordCountsOverride = overrideWeakPasswordsCount.has_value() ||
                                overrideReusedPasswordsCount.has_value() ||
                                overrideCompromisedPasswordsCount.has_value();

  // NOTE: If any password counts are overriden via Experimental
  // settings, all password counts will be considered overriden.
  if (passwordCountsOverride) {
    state.weakPasswordsCount = overrideWeakPasswordsCount.value_or(0);
    state.reusedPasswordsCount = overrideReusedPasswordsCount.value_or(0);
    state.compromisedPasswordsCount =
        overrideCompromisedPasswordsCount.value_or(0);
  } else {
    std::vector<password_manager::CredentialUIEntry> insecureCredentials =
        _safetyCheckManager->GetInsecureCredentials();

    password_manager::InsecurePasswordCounts counts =
        password_manager::CountInsecurePasswordsPerInsecureType(
            insecureCredentials);

    state.weakPasswordsCount = counts.weak_count;
    state.reusedPasswordsCount = counts.reused_count;
    state.compromisedPasswordsCount = counts.compromised_count;
  }

  state.lastRunTime = [self latestSafetyCheckRunTimestamp];

  state.runningState = CanRunSafetyCheck(state.lastRunTime)
                           ? RunningSafetyCheckState::kRunning
                           : RunningSafetyCheckState::kDefault;

  state.audience = self;
  state.safetyCheckConsumerSource = self;
  return state;
}

// Returns the last run time of the Safety Check, regardless if the check was
// started from the Safety Check (Magic Stack) module, or the Safety Check
// Settings UI.
- (std::optional<base::Time>)latestSafetyCheckRunTimestamp {
  base::Time lastRunTimeViaModule =
      _safetyCheckManager->GetLastSafetyCheckRunTime();

  base::Time lastRunTimeViaSettings =
      _localState->GetTime(prefs::kIosSettingsSafetyCheckLastRunTime);

  // Use the most recent Last Run Time—regardless of where the Safety Check was
  // run—to minimize user confusion.
  base::Time lastRunTime = lastRunTimeViaModule > lastRunTimeViaSettings
                               ? lastRunTimeViaModule
                               : lastRunTimeViaSettings;

  base::TimeDelta lastRunAge = base::Time::Now() - lastRunTime;

  // Only return the Last Run Time if the run happened within the last 24hr.
  if (lastRunAge <= TimeDelayForSafetyCheckAutorun()) {
    return lastRunTime;
  }

  return std::nullopt;
}

// Persists the current number of Safety Check issues, `issuesCount`, to
// `localPrefService`.
- (void)updateIssueCount:(NSUInteger)issuesCount
         withPrefService:(PrefService*)localPrefService {
  CHECK(localPrefService);
  CHECK(ShouldHideSafetyCheckModuleIfNoIssues());

  localPrefService->SetInteger(
      prefs::kHomeCustomizationMagicStackSafetyCheckIssuesCount, issuesCount);
}

@end