chromium/ios/chrome/browser/ui/content_suggestions/safety_check/safety_check_item_view.mm

// Copyright 2023 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_item_view.h"

#import "base/strings/string_number_conversions.h"
#import "components/version_info/version_info.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/ui/content_suggestions/safety_check/constants.h"
#import "ios/chrome/browser/ui/content_suggestions/safety_check/safety_check_item_icon.h"
#import "ios/chrome/browser/ui/content_suggestions/safety_check/types.h"
#import "ios/chrome/common/channel_info.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/table_view/table_view_cells_constants.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#import "ios/chrome/common/ui/util/dynamic_type_util.h"
#import "ios/chrome/grit/ios_branded_strings.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util.h"

namespace {

// The spacing between the title and description.
constexpr CGFloat kTitleDescriptionSpacing = 2;

// The spacing between elements within the item.
constexpr CGFloat kContentStackSpacing = 14;

// Constants related to the icon container view.
constexpr CGFloat kIconContainerSize = 56;
constexpr CGFloat kIconContainerCornerRadius = 12;

// The size of the checkmark icon.
constexpr CGFloat kCheckmarkSize = 19;
constexpr CGFloat kCheckmarkTopOffset = -6;
constexpr CGFloat kCheckmarkTrailingOffset = 6;

// The checkmark icon used for a hero-cell complete item.
UIImageView* CheckmarkIcon() {
  UIImageSymbolConfiguration* config = [UIImageSymbolConfiguration
      configurationWithWeight:UIImageSymbolWeightMedium];

  UIImageSymbolConfiguration* colorConfig =
      [UIImageSymbolConfiguration configurationWithPaletteColors:@[
        [UIColor whiteColor], [UIColor colorNamed:kGreen500Color]
      ]];

  config = [config configurationByApplyingConfiguration:colorConfig];

  UIImage* image =
      DefaultSymbolWithConfiguration(kCheckmarkCircleFillSymbol, config);

  UIImageView* icon = [[UIImageView alloc] initWithImage:image];

  icon.translatesAutoresizingMaskIntoConstraints = NO;

  [NSLayoutConstraint activateConstraints:@[
    [icon.widthAnchor constraintEqualToConstant:kCheckmarkSize],
    [icon.heightAnchor constraintEqualToAnchor:icon.widthAnchor],
  ]];

  return icon;
}

}  // namespace

@implementation SafetyCheckItemView {
  // The item layout type.
  SafetyCheckItemLayoutType _layoutType;
  // The number of weak passwords found by the Password check.
  NSInteger _weakPasswordsCount;
  // The number of reused passwords found by the Password check.
  NSInteger _reusedPasswordsCount;
  // The number of compromised passwords found by the Password check.
  NSInteger _compromisedPasswordsCount;
  // UI tap gesture recognizer.
  UITapGestureRecognizer* _tapGestureRecognizer;
}

- (instancetype)initWithItemType:(SafetyCheckItemType)itemType
                      layoutType:(SafetyCheckItemLayoutType)layoutType {
  self = [self initWithItemType:itemType
                     layoutType:layoutType
             weakPasswordsCount:0
           reusedPasswordsCount:0
      compromisedPasswordsCount:0];

  return self;
}

- (instancetype)initWithItemType:(SafetyCheckItemType)itemType
                      layoutType:(SafetyCheckItemLayoutType)layoutType
              weakPasswordsCount:(NSInteger)weakPasswordsCount
            reusedPasswordsCount:(NSInteger)reusedPasswordsCount
       compromisedPasswordsCount:(NSInteger)compromisedPasswordsCount {
  if ((self = [super init])) {
    _itemType = itemType;
    _layoutType = layoutType;
    _weakPasswordsCount = weakPasswordsCount;
    _reusedPasswordsCount = reusedPasswordsCount;
    _compromisedPasswordsCount = compromisedPasswordsCount;
  }

  return self;
}

#pragma mark - UIView

- (void)willMoveToSuperview:(UIView*)newSuperview {
  [super willMoveToSuperview:newSuperview];

  [self createSubviews];
}

- (NSString*)accessibilityLabel {
  return
      [NSString stringWithFormat:@"%@, %@", [self titleText],
                                 _layoutType == SafetyCheckItemLayoutType::kHero
                                     ? [self descriptionText]
                                     : [self compactDescriptionText]];
}

#pragma mark - Private

- (void)handleTap:(UITapGestureRecognizer*)sender {
  if (sender.state == UIGestureRecognizerStateEnded) {
    [self.tapDelegate didTapSafetyCheckItemView:self];
  }
}

// Creates all views for an individual check row in the Safety Check (Magic
// Stack) module.
- (void)createSubviews {
  // Return if the subviews have already been created and added.
  if (!(self.subviews.count == 0)) {
    return;
  }

  self.translatesAutoresizingMaskIntoConstraints = NO;
  self.accessibilityIdentifier =
      [self accessibilityIdentifierForItemType:_itemType];
  self.isAccessibilityElement = YES;
  self.accessibilityTraits = UIAccessibilityTraitButton;

  // Add a horizontal stack to contain the icon, text stack, and (optional)
  // chevron.
  NSMutableArray* arrangedSubviews = [[NSMutableArray alloc] init];

  SafetyCheckItemIcon* icon = [self iconForItemType:_itemType
                                         layoutType:_layoutType];

  // When the item is displayed in a hero-style layout, the icon is more
  // prominently displayed via an icon container view.
  if (_layoutType == SafetyCheckItemLayoutType::kHero) {
    UIView* iconContainerView = [self iconInContainer:icon];

    // Display a green checkmark when the layout is hero-cell complete.
    if (_itemType == SafetyCheckItemType::kAllSafe) {
      UIImageView* checkmark = CheckmarkIcon();

      [iconContainerView addSubview:checkmark];

      [NSLayoutConstraint activateConstraints:@[
        [checkmark.topAnchor constraintEqualToAnchor:iconContainerView.topAnchor
                                            constant:kCheckmarkTopOffset],
        [checkmark.trailingAnchor
            constraintEqualToAnchor:iconContainerView.trailingAnchor
                           constant:kCheckmarkTrailingOffset],
      ]];
    }

    [arrangedSubviews addObject:iconContainerView];
  } else {
    [arrangedSubviews addObject:icon];
  }

  UILabel* titleLabel = [self createTitleLabelForLayoutType:_layoutType];

  [titleLabel
      setContentCompressionResistancePriority:UILayoutPriorityDefaultHigh
                                      forAxis:UILayoutConstraintAxisVertical];

  UILabel* descriptionLabel = [self createDescriptionLabel];

  [descriptionLabel
      setContentCompressionResistancePriority:UILayoutPriorityDefaultLow
                                      forAxis:UILayoutConstraintAxisVertical];
  self.accessibilityLabel =
      [NSString stringWithFormat:@"%@,%@", titleLabel, descriptionLabel];

  // Add a vertical stack for the title and description labels.
  UIStackView* textStack = [[UIStackView alloc]
      initWithArrangedSubviews:@[ titleLabel, descriptionLabel ]];

  textStack.axis = UILayoutConstraintAxisVertical;
  textStack.translatesAutoresizingMaskIntoConstraints = NO;
  textStack.spacing = kTitleDescriptionSpacing;
  [textStack setContentHuggingPriority:UILayoutPriorityDefaultLow
                               forAxis:UILayoutConstraintAxisHorizontal];

  [arrangedSubviews addObject:textStack];

  // For compact layout, display a chevron at the end of the item.
  if (_layoutType == SafetyCheckItemLayoutType::kCompact) {
    UIImageView* chevron = [[UIImageView alloc]
        initWithImage:[UIImage imageNamed:@"table_view_cell_chevron"]];

    [chevron setContentHuggingPriority:UILayoutPriorityDefaultHigh
                               forAxis:UILayoutConstraintAxisHorizontal];

    [arrangedSubviews addObject:chevron];
  }

  UIStackView* contentStack =
      [[UIStackView alloc] initWithArrangedSubviews:arrangedSubviews];

  contentStack.translatesAutoresizingMaskIntoConstraints = NO;
  contentStack.axis = UILayoutConstraintAxisHorizontal;
  contentStack.alignment = UIStackViewAlignmentCenter;
  contentStack.spacing = kContentStackSpacing;

  [self addSubview:contentStack];

  AddSameConstraints(contentStack, self);

  // Set up the tap gesture recognizer.
  _tapGestureRecognizer =
      [[UITapGestureRecognizer alloc] initWithTarget:self
                                              action:@selector(handleTap:)];

  [self addGestureRecognizer:_tapGestureRecognizer];
}

// Returns the corresponding `SafetyCheckItemIcon*` given an `itemType` and
// `layoutType`.
- (SafetyCheckItemIcon*)iconForItemType:(SafetyCheckItemType)itemType
                             layoutType:(SafetyCheckItemLayoutType)layoutType {
  BOOL compactLayout = layoutType == SafetyCheckItemLayoutType::kCompact;
  BOOL inSquare = YES;

  switch (itemType) {
    case SafetyCheckItemType::kUpdateChrome:
      return
          [[SafetyCheckItemIcon alloc] initWithDefaultSymbol:kInfoCircleSymbol
                                               compactLayout:compactLayout
                                                    inSquare:inSquare];
    case SafetyCheckItemType::kPassword:
      return [[SafetyCheckItemIcon alloc] initWithCustomSymbol:kPasswordSymbol
                                                 compactLayout:compactLayout
                                                      inSquare:inSquare];
    case SafetyCheckItemType::kSafeBrowsing:
      return [[SafetyCheckItemIcon alloc] initWithCustomSymbol:kPrivacySymbol
                                                 compactLayout:compactLayout
                                                      inSquare:inSquare];
    case SafetyCheckItemType::kAllSafe:
    case SafetyCheckItemType::kRunning:
    case SafetyCheckItemType::kDefault:
      return
          [[SafetyCheckItemIcon alloc] initWithCustomSymbol:kSafetyCheckSymbol
                                              compactLayout:compactLayout
                                                   inSquare:inSquare];
  }
}

// Returns `icon` wrapped in a container view.
- (UIView*)iconInContainer:(SafetyCheckItemIcon*)icon {
  icon.translatesAutoresizingMaskIntoConstraints = NO;

  UIView* iconContainer = [[UIView alloc] init];

  iconContainer.backgroundColor = [UIColor colorNamed:kGrey100Color];
  iconContainer.layer.cornerRadius = kIconContainerCornerRadius;

  [iconContainer addSubview:icon];

  AddSameCenterConstraints(icon, iconContainer);

  [NSLayoutConstraint activateConstraints:@[
    [iconContainer.widthAnchor constraintEqualToConstant:kIconContainerSize],
    [iconContainer.widthAnchor
        constraintEqualToAnchor:iconContainer.heightAnchor],
  ]];

  return iconContainer;
}

// Creates the title label using `layoutType`.
- (UILabel*)createTitleLabelForLayoutType:
    (SafetyCheckItemLayoutType)layoutType {
  UILabel* label = [[UILabel alloc] init];

  label.text = [self titleText];
  label.translatesAutoresizingMaskIntoConstraints = NO;
  label.numberOfLines = 0;
  label.lineBreakMode = NSLineBreakByWordWrapping;
  label.font =
      layoutType == SafetyCheckItemLayoutType::kHero
          ? CreateDynamicFont(UIFontTextStyleFootnote, UIFontWeightSemibold)
          : [UIFont preferredFontForTextStyle:UIFontTextStyleFootnote];
  label.adjustsFontForContentSizeCategory = YES;
  label.textColor = [UIColor colorNamed:kTextPrimaryColor];

  return label;
}

- (NSString*)titleText {
  switch (_itemType) {
    case SafetyCheckItemType::kAllSafe:
      return l10n_util::GetNSString(IDS_IOS_SAFETY_CHECK_TITLE_ALL_SAFE);
    case SafetyCheckItemType::kRunning:
      return l10n_util::GetNSString(IDS_IOS_SAFETY_CHECK_RUNNING);
    case SafetyCheckItemType::kUpdateChrome:
      return l10n_util::GetNSString(IDS_IOS_SAFETY_CHECK_TITLE_UPDATE_CHROME);
    case SafetyCheckItemType::kPassword:
      return l10n_util::GetNSString(IDS_IOS_SAFETY_CHECK_TITLE_PASSWORD);
    case SafetyCheckItemType::kSafeBrowsing:
      return l10n_util::GetNSString(IDS_IOS_SAFETY_CHECK_TITLE_SAFE_BROWSING);
    case SafetyCheckItemType::kDefault:
      return l10n_util::GetNSString(IDS_IOS_SAFETY_CHECK_TITLE_DEFAULT);
  }
}

// Creates the description label.
- (UILabel*)createDescriptionLabel {
  UILabel* label = [[UILabel alloc] init];

  label.text = _layoutType == SafetyCheckItemLayoutType::kHero
                   ? [self descriptionText]
                   : [self compactDescriptionText];
  label.numberOfLines = 2;
  label.lineBreakMode = NSLineBreakByTruncatingTail;
  label.font = [UIFont preferredFontForTextStyle:UIFontTextStyleFootnote];
  label.adjustsFontForContentSizeCategory = YES;
  label.textColor = [UIColor colorNamed:kTextSecondaryColor];

  return label;
}

- (NSString*)descriptionText {
  switch (_itemType) {
    case SafetyCheckItemType::kAllSafe:
      return l10n_util::GetNSString(IDS_IOS_SAFETY_CHECK_DESCRIPTION_ALL_SAFE);
    case SafetyCheckItemType::kRunning:
      // The running state has no description text.
      return @"";
    case SafetyCheckItemType::kUpdateChrome:
      return [self updateChromeItemDescriptionText];
    case SafetyCheckItemType::kPassword:
      return [self passwordItemDescriptionText];
    case SafetyCheckItemType::kSafeBrowsing:
      return l10n_util::GetNSString(
          IDS_IOS_SAFETY_CHECK_DESCRIPTION_SAFE_BROWSING);
    case SafetyCheckItemType::kDefault:
      return l10n_util::GetNSString(IDS_IOS_SAFETY_CHECK_DESCRIPTION_DEFAULT);
  }
}

- (NSString*)updateChromeItemDescriptionText {
  switch (::GetChannel()) {
    case version_info::Channel::STABLE:
    case version_info::Channel::DEV:
      return l10n_util::GetNSString(
          IDS_IOS_SAFETY_CHECK_DESCRIPTION_UPDATE_CHROME);
    case version_info::Channel::BETA:
      return l10n_util::GetNSString(
          IDS_IOS_SETTINGS_SAFETY_CHECK_UPDATES_CHANNEL_BETA_DESC);
    case version_info::Channel::CANARY:
      return l10n_util::GetNSString(
          IDS_IOS_SETTINGS_SAFETY_CHECK_UPDATES_CHANNEL_CANARY_DESC);
    default:
      return l10n_util::GetNSString(
          IDS_IOS_SAFETY_CHECK_DESCRIPTION_UPDATE_CHROME);
  }
}

- (NSString*)passwordItemDescriptionText {
  if (_compromisedPasswordsCount > 1) {
    return l10n_util::GetNSStringF(
        IDS_IOS_SAFETY_CHECK_DESCRIPTION_MULTIPLE_COMPROMISED_PASSWORDS,
        base::NumberToString16(_compromisedPasswordsCount));
  }

  if (_compromisedPasswordsCount == 1) {
    return l10n_util::GetNSString(
        IDS_IOS_SAFETY_CHECK_DESCRIPTION_COMPROMISED_PASSWORD);
  }

  if (_reusedPasswordsCount > 1) {
    return l10n_util::GetNSStringF(
        IDS_IOS_SAFETY_CHECK_DESCRIPTION_MULTIPLE_REUSED_PASSWORDS,
        base::NumberToString16(_reusedPasswordsCount));
  }

  if (_reusedPasswordsCount == 1) {
    return l10n_util::GetNSString(
        IDS_IOS_SAFETY_CHECK_DESCRIPTION_REUSED_PASSWORD);
  }

  if (_weakPasswordsCount > 1) {
    return l10n_util::GetNSStringF(
        IDS_IOS_SAFETY_CHECK_DESCRIPTION_MULTIPLE_WEAK_PASSWORDS,
        base::NumberToString16(_weakPasswordsCount));
  }

  if (_weakPasswordsCount == 1) {
    return l10n_util::GetNSString(
        IDS_IOS_SAFETY_CHECK_DESCRIPTION_WEAK_PASSWORD);
  }

  return l10n_util::GetNSString(
      IDS_IOS_SAFETY_CHECK_COMPACT_DESCRIPTION_MULTIPLE_PASSWORD_ISSUES);
}

- (NSString*)compactDescriptionText {
  switch (_itemType) {
    case SafetyCheckItemType::kAllSafe:
      return l10n_util::GetNSString(IDS_IOS_SAFETY_CHECK_DESCRIPTION_ALL_SAFE);
    case SafetyCheckItemType::kRunning:
      // The running state has no description text.
      return @"";
    case SafetyCheckItemType::kUpdateChrome:
      return [self updateChromeItemCompactDescriptionText];
    case SafetyCheckItemType::kPassword:
      return [self passwordItemCompactDescriptionText];
    case SafetyCheckItemType::kSafeBrowsing:
      return l10n_util::GetNSString(
          IDS_IOS_SAFETY_CHECK_COMPACT_DESCRIPTION_SAFE_BROWSING);
    case SafetyCheckItemType::kDefault:
      return l10n_util::GetNSString(IDS_IOS_SAFETY_CHECK_TITLE_DEFAULT);
  }
}

- (NSString*)updateChromeItemCompactDescriptionText {
  switch (::GetChannel()) {
    case version_info::Channel::STABLE:
    case version_info::Channel::DEV:
      return l10n_util::GetNSString(
          IDS_IOS_SAFETY_CHECK_COMPACT_DESCRIPTION_UPDATE_CHROME);
    case version_info::Channel::BETA:
      return l10n_util::GetNSString(
          IDS_IOS_SETTINGS_SAFETY_CHECK_UPDATES_CHANNEL_BETA_DESC);
    case version_info::Channel::CANARY:
      return l10n_util::GetNSString(
          IDS_IOS_SETTINGS_SAFETY_CHECK_UPDATES_CHANNEL_CANARY_DESC);
    default:
      return l10n_util::GetNSString(
          IDS_IOS_SAFETY_CHECK_COMPACT_DESCRIPTION_UPDATE_CHROME);
  }
}

- (NSString*)passwordItemCompactDescriptionText {
  if (_compromisedPasswordsCount >= 1) {
    return l10n_util::GetNSString(
        IDS_IOS_SAFETY_CHECK_COMPACT_DESCRIPTION_COMPROMISED_PASSWORD);
  }

  if (_reusedPasswordsCount >= 1) {
    return l10n_util::GetNSString(
        IDS_IOS_SAFETY_CHECK_COMPACT_DESCRIPTION_REUSED_PASSWORD);
  }

  if (_weakPasswordsCount >= 1) {
    return l10n_util::GetNSString(
        IDS_IOS_SAFETY_CHECK_COMPACT_DESCRIPTION_WEAK_PASSWORD);
  }

  return l10n_util::GetNSString(
      IDS_IOS_SAFETY_CHECK_COMPACT_DESCRIPTION_MULTIPLE_PASSWORD_ISSUES);
}

- (NSString*)accessibilityIdentifierForItemType:(SafetyCheckItemType)itemType {
  switch (itemType) {
    case SafetyCheckItemType::kAllSafe:
      return safety_check::kAllSafeItemID;
    case SafetyCheckItemType::kRunning:
      return safety_check::kRunningItemID;
    case SafetyCheckItemType::kUpdateChrome:
      return safety_check::kUpdateChromeItemID;
    case SafetyCheckItemType::kPassword:
      return safety_check::kPasswordItemID;
    case SafetyCheckItemType::kSafeBrowsing:
      return safety_check::kSafeBrowsingItemID;
    case SafetyCheckItemType::kDefault:
      return safety_check::kDefaultItemID;
  }
}

@end