chromium/ios/chrome/browser/incognito_interstitial/ui_bundled/incognito_interstitial_view_controller.mm

// Copyright 2022 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/incognito_interstitial/ui_bundled/incognito_interstitial_view_controller.h"

#import <algorithm>

#import "base/apple/foundation_util.h"
#import "base/check.h"
#import "base/ios/ios_util.h"
#import "ios/chrome/browser/incognito_interstitial/ui_bundled/incognito_interstitial_constants.h"
#import "ios/chrome/browser/ntp/ui_bundled/incognito/incognito_view.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/ui/elements/extended_touch_target_button.h"
#import "ios/chrome/browser/shared/ui/util/attributed_string_util.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#import "ios/chrome/grit/ios_branded_strings.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/device_form_factor.h"
#import "ui/base/l10n/l10n_util_mac.h"

namespace {

// Opacity of navigation bar should be 0% at offset keyframe 0 and 100% at
// keyframe 1.
const CGFloat kNavigationBarFadeInKeyFrame0 = 70;
const CGFloat kNavigationBarFadeInKeyFrame1 = 140;
const CGFloat kNavigationBarFadeInCompactHeightKeyFrame0 = 20;
const CGFloat kNavigationBarFadeInCompactHeightKeyFrame1 = 50;

// Name of banner at the top of the view.
NSString* const kIncognitoInterstitialBannerName =
    @"incognito_interstitial_screen_banner";

// Maximum number of lines for the URL label, before the user unfolds it.
const int kURLLabelDefaultNumberOfLines = 3;

// Line height multiple for the title label.
const CGFloat kTitleLabelLineHeightMultiple = 1.3;

}  // namespace

@interface IncognitoInterstitialViewController ()

// The navigation bar to display at the top of the view, to contain a "Cancel"
// button.
@property(nonatomic, strong) UINavigationBar* navigationBar;

// Vertical offset of internal scroll view, to update navigation bar opacity.
@property(nonatomic, assign) CGFloat scrollViewContentOffsetY;

// Label to display the URL which is going to be opened.
@property(nonatomic, strong) UILabel* URLLabel;

// Button which allows the user to remove the limit on the number of lines
// for `URLLabel`.
@property(nonatomic, strong) UIButton* expandURLButton;

// Whether the number of lines of `URLLabel` is unlimited.
@property(nonatomic, assign) BOOL URLIsExpanded;

@end

@implementation IncognitoInterstitialViewController

@dynamic delegate;

#pragma mark - UIViewController

- (void)viewDidLoad {
  self.view.accessibilityIdentifier =
      kIncognitoInterstitialAccessibilityIdentifier;

  self.bannerName = kIncognitoInterstitialBannerName;
  self.bannerSize = BannerImageSizeType::kStandard;
  self.shouldBannerFillTopSpace = YES;
  self.shouldHideBanner = IsCompactHeight(self.traitCollection);

  NSString* title =
      l10n_util::GetNSString(IDS_IOS_INCOGNITO_INTERSTITIAL_TITLE);
  self.title = title;
  self.titleText = title;
  self.titleHorizontalMargin = 0;
  self.primaryActionString = l10n_util::GetNSString(
      IDS_IOS_INCOGNITO_INTERSTITIAL_OPEN_IN_CHROME_INCOGNITO);
  self.secondaryActionString =
      l10n_util::GetNSString(IDS_IOS_INCOGNITO_INTERSTITIAL_OPEN_IN_CHROME);

  // This needs to be called after parameters of `PromoStyleViewController` have
  // been set, but before adding additional layout constraints, since these
  // constraints can only be activated once the complete view hierarchy has been
  // constructed and relevant views belong to the same hierarchy.
  [super viewDidLoad];

  // Fix the line height multiple of `self.titleLabel`.
  [self fixTitleLabelLineHeightMultiple];

  self.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;
  self.modalInPresentation = YES;

  // Creating the Incognito view (same one as NTP).
  IncognitoView* incognitoView =
      [[IncognitoView alloc] initWithFrame:CGRectZero
             showTopIncognitoImageAndTitle:NO
                 stackViewHorizontalMargin:0
                         stackViewMaxWidth:CGFLOAT_MAX];
  incognitoView.URLLoaderDelegate = self.URLLoaderDelegate;
  incognitoView.translatesAutoresizingMaskIntoConstraints = NO;
  incognitoView.bounces = NO;

  // The Incognito view is put inside a container because it might try
  // to put constraints on its superview.
  UIView* incognitoViewContainer = [[UIView alloc] init];
  incognitoViewContainer.translatesAutoresizingMaskIntoConstraints = NO;
  [incognitoViewContainer addSubview:incognitoView];

  // A stack view is created to contain the URL label as well as the
  // Incognito view container.
  UIStackView* stackView = [[UIStackView alloc]
      initWithArrangedSubviews:@[ self.URLLabel, incognitoViewContainer ]];
  stackView.translatesAutoresizingMaskIntoConstraints = NO;
  stackView.axis = UILayoutConstraintAxisVertical;
  [self.specificContentView addSubview:stackView];

  // Create the gradient view located on the leading end of the "more" button
  // which lets the user unfold the URL label.
  UIView* gradientView = [[UIView alloc]
      initWithFrame:CGRectMake(0, 0, self.URLLabel.font.lineHeight,
                               self.URLLabel.font.lineHeight)];
  gradientView.translatesAutoresizingMaskIntoConstraints = NO;
  gradientView.backgroundColor = self.view.backgroundColor;
  CAGradientLayer* gradientLayer = [CAGradientLayer layer];
  gradientLayer.frame = gradientView.bounds;
  gradientLayer.startPoint = CGPointMake(0.0, 0.0);
  gradientLayer.endPoint = CGPointMake(1.0, 0.0);
  gradientLayer.colors =
      @[ (id)[UIColor clearColor].CGColor, (id)[UIColor whiteColor].CGColor ];
  gradientView.layer.mask = gradientLayer;
  [self.expandURLButton addSubview:gradientView];
  // Add the "more" button to the content view.
  [self.specificContentView addSubview:self.expandURLButton];

  UIBarButtonItem* cancelButton = [[UIBarButtonItem alloc]
      initWithBarButtonSystemItem:UIBarButtonSystemItemCancel
                           target:self.delegate
                           action:@selector(didTapCancelButton)];
  cancelButton.accessibilityIdentifier =
      kIncognitoInterstitialCancelButtonAccessibilityIdentifier;

  UINavigationItem* navigationRootItem =
      [[UINavigationItem alloc] initWithTitle:@""];
  navigationRootItem.rightBarButtonItem = cancelButton;

  self.navigationBar = [[UINavigationBar alloc] init];
  [self.navigationBar pushNavigationItem:navigationRootItem animated:false];
  [self updateNavigationBarAppearance];

  [NSLayoutConstraint activateConstraints:@[
    [stackView.leadingAnchor
        constraintEqualToAnchor:self.specificContentView.leadingAnchor],
    [stackView.trailingAnchor
        constraintEqualToAnchor:self.specificContentView.trailingAnchor],
    [stackView.topAnchor
        constraintEqualToAnchor:self.specificContentView.topAnchor],
    [stackView.bottomAnchor
        constraintEqualToAnchor:self.specificContentView.bottomAnchor],
    [incognitoView.heightAnchor
        constraintEqualToAnchor:incognitoView.contentLayoutGuide.heightAnchor],
    [incognitoViewContainer.leadingAnchor
        constraintEqualToAnchor:incognitoView.leadingAnchor],
    [incognitoViewContainer.trailingAnchor
        constraintEqualToAnchor:incognitoView.trailingAnchor],
    [incognitoViewContainer.topAnchor
        constraintEqualToAnchor:incognitoView.topAnchor],
    [incognitoViewContainer.bottomAnchor
        constraintEqualToAnchor:incognitoView.bottomAnchor],
    [gradientView.trailingAnchor
        constraintEqualToAnchor:self.expandURLButton.leadingAnchor],
    [gradientView.bottomAnchor
        constraintEqualToAnchor:self.expandURLButton.bottomAnchor],
    [gradientView.topAnchor
        constraintEqualToAnchor:self.expandURLButton.topAnchor],
    [gradientView.widthAnchor
        constraintEqualToAnchor:gradientView.heightAnchor],
    [self.expandURLButton.trailingAnchor
        constraintEqualToAnchor:self.URLLabel.trailingAnchor],
    [self.expandURLButton.bottomAnchor
        constraintEqualToAnchor:self.URLLabel.bottomAnchor],
  ]];

  [self.view addSubview:self.navigationBar];
  self.navigationBar.translatesAutoresizingMaskIntoConstraints = NO;
  [NSLayoutConstraint activateConstraints:@[
    [self.navigationBar.leadingAnchor
        constraintEqualToAnchor:self.view.leadingAnchor],
    [self.navigationBar.trailingAnchor
        constraintEqualToAnchor:self.view.trailingAnchor],
    [self.navigationBar.topAnchor constraintEqualToAnchor:self.view.topAnchor],
  ]];
}

- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
  [super traitCollectionDidChange:previousTraitCollection];
  self.shouldHideBanner = IsCompactHeight(self.traitCollection);
  [self updateNavigationBarAppearance];
}

- (void)viewDidLayoutSubviews {
  if (self.URLIsExpanded) {
    self.expandURLButton.hidden = YES;
    self.URLLabel.numberOfLines = 0;
  } else {
    CGRect URLLabelBoundingRect = [self.URLLabel.text
        boundingRectWithSize:CGSizeMake(self.URLLabel.bounds.size.width,
                                        CGFLOAT_MAX)
                     options:NSStringDrawingUsesLineFragmentOrigin
                  attributes:@{NSFontAttributeName : self.URLLabel.font}
                     context:nil];
    int expandedNumberOfLines =
        URLLabelBoundingRect.size.height / self.URLLabel.font.lineHeight;
    self.expandURLButton.hidden =
        (expandedNumberOfLines <= kURLLabelDefaultNumberOfLines);
  }
}

- (void)viewDidAppear:(BOOL)animated {
  [super viewDidAppear:animated];
  // Ensure the title label is focused when the Incognito interstial appears.
  UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification,
                                  self.titleLabel);
}

#pragma mark - Accessors

- (UILabel*)URLLabel {
  if (!_URLLabel) {
    _URLLabel = [[UILabel alloc] initWithFrame:self.view.frame];
    _URLLabel.lineBreakMode = NSLineBreakByClipping;
    _URLLabel.text = self.URLText;
    _URLLabel.numberOfLines = kURLLabelDefaultNumberOfLines;
    _URLLabel.textAlignment = NSTextAlignmentCenter;
    _URLLabel.translatesAutoresizingMaskIntoConstraints = NO;
    _URLLabel.accessibilityIdentifier =
        kIncognitoInterstitialURLLabelAccessibilityIdentifier;
  }
  return _URLLabel;
}

- (UIButton*)expandURLButton {
  if (!_expandURLButton) {
    __weak __typeof(self) weakSelf = self;
    UIAction* readMoreAction = [UIAction actionWithHandler:^(id sender) {
      [weakSelf expandURLButtonWasTapped];
    }];

    NSAttributedString* readMoreString = [[NSAttributedString alloc]
        initWithString:l10n_util::GetNSString(
                           IDS_IOS_INCOGNITO_INTERSTITIAL_URL_READ_MORE_BUTTON)
            attributes:@{
              NSFontAttributeName : _URLLabel.font,
              NSForegroundColorAttributeName : [UIColor colorNamed:kBlueColor]
            }];

    _expandURLButton =
        [[ExtendedTouchTargetButton alloc] initWithFrame:CGRectZero
                                           primaryAction:readMoreAction];

    UIButtonConfiguration* buttonConfiguration =
        [UIButtonConfiguration plainButtonConfiguration];
    buttonConfiguration.contentInsets = NSDirectionalEdgeInsetsMake(0, 0, 0, 0);
    buttonConfiguration.attributedTitle = readMoreString;
    _expandURLButton.configuration = buttonConfiguration;

    _expandURLButton.backgroundColor = self.view.backgroundColor;
    _expandURLButton.translatesAutoresizingMaskIntoConstraints = NO;
    // On voice over, the full info is on the URL field and this button isn't
    // needed.
    _expandURLButton.accessibilityElementsHidden = YES;
  }
  return _expandURLButton;
}

#pragma mark - UIScrollViewDelegate

// This override allows scroll detection of the scroll view contained within the
// underlying PromoStyleViewController.
- (void)scrollViewDidScroll:(UIScrollView*)scrollView {
  // Constrain vertical content offset to positive values only.
  CGPoint contentOffset = scrollView.contentOffset;
  contentOffset.y = fmax(0, contentOffset.y);
  scrollView.contentOffset = contentOffset;

  self.scrollViewContentOffsetY = scrollView.contentOffset.y;
  [self updateNavigationBarAppearance];

  [super scrollViewDidScroll:scrollView];
}

#pragma mark - Private

- (void)updateNavigationBarAppearance {
  CGFloat keyFrame0 = IsCompactHeight(self.traitCollection)
                          ? kNavigationBarFadeInCompactHeightKeyFrame0
                          : kNavigationBarFadeInKeyFrame0;
  CGFloat keyFrame1 = IsCompactHeight(self.traitCollection)
                          ? kNavigationBarFadeInCompactHeightKeyFrame1
                          : kNavigationBarFadeInKeyFrame1;
  CGFloat opacity =
      (self.scrollViewContentOffsetY - keyFrame0) / (keyFrame1 - keyFrame0);
  opacity = std::clamp(opacity, 0.0, 1.0, std::less_equal<>());

  UIColor* backgroundColor =
      [UIColor colorNamed:kGroupedPrimaryBackgroundColor];
  UIColor* shadowColor = [UIColor colorNamed:kSeparatorColor];
  CGFloat backgroundAlpha = CGColorGetAlpha(backgroundColor.CGColor);
  CGFloat shadowAlpha = CGColorGetAlpha(shadowColor.CGColor);

  UINavigationBarAppearance* appearance =
      [[UINavigationBarAppearance alloc] init];
  [appearance configureWithOpaqueBackground];
  appearance.backgroundColor =
      [backgroundColor colorWithAlphaComponent:backgroundAlpha * opacity];
  appearance.shadowColor =
      [shadowColor colorWithAlphaComponent:shadowAlpha * opacity];

  self.navigationBar.compactAppearance = appearance;
  self.navigationBar.standardAppearance = appearance;
  self.navigationBar.scrollEdgeAppearance = appearance;

  self.navigationBar.tintColor = [UIColor colorNamed:kBlueColor];
}

// Called when `expandURLButton` is tapped.
- (void)expandURLButtonWasTapped {
  self.URLIsExpanded = YES;
  [self.view setNeedsLayout];
}

// Set the `attributedText` attribute of `self.titleLabel` to customize the line
// height multiple.
- (void)fixTitleLabelLineHeightMultiple {
  NSMutableAttributedString* titleAttributedText =
      [NSAttributedStringFromUILabel(self.titleLabel) mutableCopy];
  NSMutableDictionary* attributes = [NSMutableDictionary
      dictionaryWithDictionary:[titleAttributedText attributesAtIndex:0
                                                       effectiveRange:nil]];
  NSMutableParagraphStyle* paragraphStyle =
      [[NSMutableParagraphStyle alloc] init];
  [paragraphStyle setParagraphStyle:attributes[NSParagraphStyleAttributeName]];
  paragraphStyle.lineHeightMultiple = kTitleLabelLineHeightMultiple;
  attributes[NSParagraphStyleAttributeName] = paragraphStyle;
  [titleAttributedText
      setAttributes:attributes
              range:NSMakeRange(0, titleAttributedText.length)];
  self.titleLabel.attributedText = titleAttributedText;
}

@end