chromium/ios/chrome/browser/tips_notifications/model/tips_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/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/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 "ios/chrome/browser/default_browser/model/promo_source.h"
#import "ios/chrome/browser/default_browser/model/utils.h"
#import "ios/chrome/browser/feature_engagement/model/tracker_factory.h"
#import "ios/chrome/browser/ntp/model/set_up_list_prefs.h"
#import "ios/chrome/browser/push_notification/model/constants.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/commands/application_commands.h"
#import "ios/chrome/browser/shared/public/commands/browser_coordinator_commands.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/commands/docking_promo_commands.h"
#import "ios/chrome/browser/shared/public/commands/settings_commands.h"
#import "ios/chrome/browser/shared/public/commands/show_signin_command.h"
#import "ios/chrome/browser/shared/public/commands/whats_new_commands.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/signin/model/chrome_account_manager_service.h"
#import "ios/chrome/browser/signin/model/chrome_account_manager_service_factory.h"
#import "ios/chrome/browser/tips_notifications/model/utils.h"
#import "ios/chrome/browser/ui/authentication/signin_presenter.h"
#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_commands.h"
#import "ios/chrome/browser/ui/content_suggestions/set_up_list/utils.h"
#import "ui/base/device_form_factor.h"

namespace {

// 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 signin is allowed / enabled.
bool IsSigninEnabled(AuthenticationService* auth_service) {
  switch (auth_service->GetServiceStatus()) {
    case AuthenticationService::ServiceStatus::SigninForcedByPolicy:
    case AuthenticationService::ServiceStatus::SigninAllowed:
      return true;
    case AuthenticationService::ServiceStatus::SigninDisabledByUser:
    case AuthenticationService::ServiceStatus::SigninDisabledByPolicy:
    case AuthenticationService::ServiceStatus::SigninDisabledByInternal:
      return false;
  }
}

// Returns true if a Default Browser Promo was canceled.
bool DefaultBrowserPromoCanceled() {
  std::optional<IOSDefaultBrowserPromoAction> action =
      DefaultBrowserPromoLastAction();
  if (!action.has_value()) {
    return false;
  }

  switch (action.value()) {
    case IOSDefaultBrowserPromoAction::kCancel:
      return true;
    case IOSDefaultBrowserPromoAction::kActionButton:
    case IOSDefaultBrowserPromoAction::kRemindMeLater:
    case IOSDefaultBrowserPromoAction::kDismiss:
      return false;
  }
}

// Returns true if the Feature Engagement Tracker has ever triggered for the
// given `feature`.
bool FETHasEverTriggered(Browser* browser, const base::Feature& feature) {
  feature_engagement::Tracker* tracker =
      feature_engagement::TrackerFactory::GetForBrowserState(
          browser->GetBrowserState());
  return tracker->HasEverTriggered(feature, true);
}

// Returns the user's type stored in local state prefs.
TipsNotificationUserType GetUserType() {
  PrefService* local_state = GetApplicationContext()->GetLocalState();
  return static_cast<TipsNotificationUserType>(
      local_state->GetInteger(kTipsNotificationsUserType));
}

// Sets the user's type in local state prefs, and records a histogram with the
// type.
void SetUserType(TipsNotificationUserType user_type) {
  PrefService* local_state = GetApplicationContext()->GetLocalState();
  local_state->SetInteger(kTipsNotificationsUserType, int(user_type));
  base::UmaHistogramEnumeration("IOS.Notifications.Tips.UserType", user_type);
}

}  // namespace

TipsNotificationClient::TipsNotificationClient()
    : PushNotificationClient(PushNotificationClientId::kTips) {
  pref_change_registrar_.Init(GetApplicationContext()->GetLocalState());
  PrefChangeRegistrar::NamedChangeCallback pref_callback = base::BindRepeating(
      &TipsNotificationClient::OnPermittedPrefChanged, base::Unretained(this));
  pref_change_registrar_.Add(prefs::kAppLevelPushNotificationPermissions,
                             pref_callback);
  permitted_ = IsPermitted();
  user_type_ = GetUserType();
}

TipsNotificationClient::~TipsNotificationClient() = default;

void TipsNotificationClient::HandleNotificationInteraction(
    UNNotificationResponse* response) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!IsTipsNotification(response.notification.request)) {
    return;
  }

  interacted_type_ = ParseTipsNotificationType(response.notification.request);
  if (!interacted_type_.has_value()) {
    base::UmaHistogramEnumeration("IOS.Notifications.Tips.Interaction",
                                  TipsNotificationType::kError);
    return;
  }
  base::UmaHistogramEnumeration("IOS.Notifications.Tips.Interaction",
                                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());
  }
}

void TipsNotificationClient::HandleNotificationInteraction(
    TipsNotificationType type) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  id<ApplicationCommands> application_handler =
      HandlerForProtocol(Dispatcher(), ApplicationCommands);
  [application_handler
      prepareToPresentModal:
          base::CallbackToBlock(
              base::BindOnce(&TipsNotificationClient::ShowUIForNotificationType,
                             weak_ptr_factory_.GetWeakPtr(), type))];
}

UIBackgroundFetchResult TipsNotificationClient::HandleNotificationReception(
    NSDictionary<NSString*, id>* notification) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  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_);
  if (user_type_ == TipsNotificationUserType::kUnknown) {
    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()) {
    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_) {
    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);
}

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_);
  if (!request) {
    MaybeLogTriggeredNotification();
    MaybeLogDismissedNotification();
    interacted_type_ = std::nullopt;
    MaybeRequestNotification(base::DoNothing());
    return;
  }

  MaybeLogDismissedNotification();
  interacted_type_ = std::nullopt;
}

void TipsNotificationClient::MaybeRequestNotification(
    base::OnceClosure completion) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!permitted_) {
    std::move(completion).Run();
    return;
  }

  PrefService* local_state = GetApplicationContext()->GetLocalState();
  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.
  static const TipsNotificationType kTypes[] = {
      TipsNotificationType::kSetUpListContinuation,
      TipsNotificationType::kWhatsNew,
      TipsNotificationType::kOmniboxPosition,
      TipsNotificationType::kDefaultBrowser,
      TipsNotificationType::kDocking,
      TipsNotificationType::kSignin,
  };

  for (TipsNotificationType type : kTypes) {
    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 (ShouldSendNotification(type)) {
      RequestNotification(type, std::move(completion));
      return;
    }
  }
  std::move(completion).Run();
}

void TipsNotificationClient::ClearAllRequestedNotifications() {
  [UNUserNotificationCenter.currentNotificationCenter
      removePendingNotificationRequestsWithIdentifiers:@[
        kTipsNotificationId
      ]];
}

void TipsNotificationClient::RequestNotification(TipsNotificationType type,
                                                 base::OnceClosure completion) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  UNNotificationRequest* request = TipsNotificationRequest(type, user_type_);

  auto completion_block = base::CallbackToBlock(base::BindPostTask(
      base::SequencedTaskRunner::GetCurrentDefault(),
      base::BindOnce(&TipsNotificationClient::OnNotificationRequested,
                     weak_ptr_factory_.GetWeakPtr(), type)
          .Then(std::move(completion))));

  [UNUserNotificationCenter.currentNotificationCenter
      addNotificationRequest:request
       withCompletionHandler:completion_block];
  MarkNotificationTypeSent(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::ShouldSendNotification(TipsNotificationType type) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  switch (type) {
    case TipsNotificationType::kDefaultBrowser:
      return ShouldSendDefaultBrowser();
    case TipsNotificationType::kWhatsNew:
      return ShouldSendWhatsNew();
    case TipsNotificationType::kSignin:
      return ShouldSendSignin();
    case TipsNotificationType::kSetUpListContinuation:
      return ShouldSendSetUpListContinuation();
    case TipsNotificationType::kDocking:
      return ShouldSendDocking();
    case TipsNotificationType::kOmniboxPosition:
      return ShouldSendOmniboxPosition();
    case TipsNotificationType::kLens:
    case TipsNotificationType::kEnhancedSafeBrowsing:
    case TipsNotificationType::kError:
      NOTREACHED();
  }
}

bool TipsNotificationClient::ShouldSendDefaultBrowser() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return !IsChromeLikelyDefaultBrowser() && !DefaultBrowserPromoCanceled();
}

bool TipsNotificationClient::ShouldSendWhatsNew() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  Browser* browser = GetSceneLevelForegroundActiveBrowser();
  if (!browser) {
    return false;
  }
  return !FETHasEverTriggered(browser,
                              feature_engagement::kIPHWhatsNewUpdatedFeature);
}

bool TipsNotificationClient::ShouldSendSignin() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  Browser* browser = GetSceneLevelForegroundActiveBrowser();
  if (!browser) {
    return false;
  }
  ChromeBrowserState* browser_state = browser->GetBrowserState();
  AuthenticationService* auth_service =
      AuthenticationServiceFactory::GetForBrowserState(browser_state);

  return IsSigninEnabled(auth_service) &&
         !auth_service->HasPrimaryIdentity(signin::ConsentLevel::kSignin);
}

bool TipsNotificationClient::ShouldSendSetUpListContinuation() {
  Browser* browser = GetSceneLevelForegroundActiveBrowser();
  if (!browser) {
    return false;
  }
  PrefService* local_prefs = GetApplicationContext()->GetLocalState();
  PrefService* user_prefs = browser->GetBrowserState()->GetPrefs();
  if (!set_up_list_utils::IsSetUpListActive(local_prefs, user_prefs)) {
    return false;
  }

  // The Set Up List only shows for 14 days after FirstRun, so this
  // notification should only be requested 14 days minus the trigger interval
  // after FirstRun.
  if (!IsFirstRunRecent(base::Days(14) -
                        TipsNotificationTriggerDelta(user_type_))) {
    return false;
  }
  return !set_up_list_prefs::AllItemsComplete(local_prefs);
}

bool TipsNotificationClient::ShouldSendDocking() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  Browser* browser = GetSceneLevelForegroundActiveBrowser();
  if (!browser) {
    return false;
  }
  return !FETHasEverTriggered(browser,
                              feature_engagement::kIPHiOSDockingPromoFeature) &&
         !FETHasEverTriggered(
             browser,
             feature_engagement::kIPHiOSDockingPromoRemindMeLaterFeature);
}

bool TipsNotificationClient::ShouldSendOmniboxPosition() {
  // OmniboxPositionChoice is only available on phones.
  if (ui::GetDeviceFormFactor() != ui::DEVICE_FORM_FACTOR_PHONE) {
    return false;
  }
  return !GetApplicationContext()->GetLocalState()->GetUserPrefValue(
      prefs::kBottomOmnibox);
}

bool TipsNotificationClient::IsSceneLevelForegroundActive() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return GetSceneLevelForegroundActiveBrowser() != nullptr;
}

CommandDispatcher* TipsNotificationClient::Dispatcher() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return GetSceneLevelForegroundActiveBrowser()->GetCommandDispatcher();
}

void TipsNotificationClient::ShowUIForNotificationType(
    TipsNotificationType type) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  switch (type) {
    case TipsNotificationType::kDefaultBrowser:
      ShowDefaultBrowserPromo();
      break;
    case TipsNotificationType::kWhatsNew:
      ShowWhatsNew();
      break;
    case TipsNotificationType::kSignin:
      ShowSignin();
      break;
    case TipsNotificationType::kSetUpListContinuation:
      ShowSetUpListContinuation();
      break;
    case TipsNotificationType::kDocking:
      ShowDocking();
      break;
    case TipsNotificationType::kOmniboxPosition:
      ShowOmniboxPosition();
      break;
    case TipsNotificationType::kLens:
      ShowLensPromo();
      break;
    case TipsNotificationType::kEnhancedSafeBrowsing:
    case TipsNotificationType::kError:
      NOTREACHED();
  }
}

void TipsNotificationClient::ShowDefaultBrowserPromo() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  id<SettingsCommands> settings_handler =
      HandlerForProtocol(Dispatcher(), SettingsCommands);
  [settings_handler
      showDefaultBrowserSettingsFromViewController:nil
                                      sourceForUMA:
                                          DefaultBrowserSettingsPageSource::
                                              kTipsNotification];
}

void TipsNotificationClient::ShowWhatsNew() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  [HandlerForProtocol(Dispatcher(), WhatsNewCommands) showWhatsNew];
}

void TipsNotificationClient::ShowSignin() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  Browser* browser = GetSceneLevelForegroundActiveBrowser();
  // If there are 0 identities, kInstantSignin requires less taps.
  ChromeBrowserState* browser_state = browser->GetBrowserState();
  AuthenticationOperation operation =
      ChromeAccountManagerServiceFactory::GetForBrowserState(browser_state)
              ->HasIdentities()
          ? AuthenticationOperation::kSigninOnly
          : AuthenticationOperation::kInstantSignin;
  ShowSigninCommand* command = [[ShowSigninCommand alloc]
      initWithOperation:operation
               identity:nil
            accessPoint:signin_metrics::AccessPoint::
                            ACCESS_POINT_TIPS_NOTIFICATION
            promoAction:signin_metrics::PromoAction::
                            PROMO_ACTION_NO_SIGNIN_PROMO
               callback:nil];

  [HandlerForProtocol(Dispatcher(), SigninPresenter) showSignin:command];
}

void TipsNotificationClient::ShowSetUpListContinuation() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  [HandlerForProtocol(Dispatcher(), ContentSuggestionsCommands)
      showSetUpListSeeMoreMenu];
}

void TipsNotificationClient::ShowDocking() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  [HandlerForProtocol(Dispatcher(), DockingPromoCommands) showDockingPromo:YES];
}

void TipsNotificationClient::ShowOmniboxPosition() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  [HandlerForProtocol(Dispatcher(), BrowserCoordinatorCommands)
      showOmniboxPositionChoice];
}

void TipsNotificationClient::ShowLensPromo() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  [HandlerForProtocol(Dispatcher(), BrowserCoordinatorCommands) showLensPromo];
}

void TipsNotificationClient::MarkNotificationTypeSent(
    TipsNotificationType type) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  PrefService* local_state = GetApplicationContext()->GetLocalState();
  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());
  base::UmaHistogramEnumeration("IOS.Notifications.Tips.Sent", type);
}

void TipsNotificationClient::MarkNotificationTypeNotSent(
    TipsNotificationType type) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  PrefService* local_state = GetApplicationContext()->GetLocalState();
  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_);
  PrefService* local_state = GetApplicationContext()->GetLocalState();
  const PrefService::Preference* last_sent =
      local_state->FindPreference(kTipsNotificationsLastSent);
  if (last_sent->IsDefaultValue()) {
    return;
  }

  TipsNotificationType type =
      static_cast<TipsNotificationType>(last_sent->GetValue()->GetInt());
  base::UmaHistogramEnumeration("IOS.Notifications.Tips.Triggered", type);
  local_state->SetInteger(kTipsNotificationsLastTriggered, int(type));
  local_state->ClearPref(kTipsNotificationsLastSent);
}

void TipsNotificationClient::MaybeLogDismissedNotification() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  PrefService* local_state = GetApplicationContext()->GetLocalState();
  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.
  PrefService* local_state = GetApplicationContext()->GetLocalState();
  TipsNotificationType type = static_cast<TipsNotificationType>(
      local_state->GetInteger(kTipsNotificationsLastTriggered));
  base::UmaHistogramEnumeration("IOS.Notifications.Tips.Dismissed", type);
  local_state->ClearPref(kTipsNotificationsLastTriggered);
}

bool TipsNotificationClient::IsPermitted() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // TODO(crbug.com/325279788): use
  // GetMobileNotificationPermissionStatusForClient to determine opt-in
  // state.
  PrefService* local_state = GetApplicationContext()->GetLocalState();
  return local_state->GetDict(prefs::kAppLevelPushNotificationPermissions)
      .FindBool(kTipsNotificationKey)
      .value_or(false);
}

void TipsNotificationClient::OnPermittedPrefChanged(const std::string& name) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  bool newpermitted_ = IsPermitted();
  if (permitted_ != newpermitted_ && IsSceneLevelForegroundActive()) {
    ClearAllRequestedNotifications();
    CheckAndMaybeRequestNotification(base::DoNothing());
  }
}

void TipsNotificationClient::ClassifyUser() {
  PrefService* local_state = GetApplicationContext()->GetLocalState();

  if (!local_state->GetUserPrefValue(kTipsNotificationsLastRequestedTime)) {
    return;
  }

  base::Time now = base::Time::Now();
  base::Time last_request =
      local_state->GetTime(kTipsNotificationsLastRequestedTime);
  if (now < last_request + base::Hours(2)) {
    // Not enough time has passed to classify the user.
    return;
  }

  if (now > last_request + TipsNotificationTriggerDelta(
                               TipsNotificationUserType::kUnknown)) {
    user_type_ = TipsNotificationUserType::kLessEngaged;
  } else {
    user_type_ = TipsNotificationUserType::kActiveSeeker;
  }
  SetUserType(user_type_);
}