chromium/ios/chrome/browser/overlays/ui_bundled/infobar_banner/infobar_banner_overlay_coordinator.mm

// Copyright 2019 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/overlays/ui_bundled/infobar_banner/infobar_banner_overlay_coordinator.h"

#import "base/apple/foundation_util.h"
#import "base/check.h"
#import "ios/chrome/browser/feature_engagement/model/tracker_factory.h"
#import "ios/chrome/browser/infobars/model/infobar_ios.h"
#import "ios/chrome/browser/infobars/model/infobar_type.h"
#import "ios/chrome/browser/overlays/model/public/common/infobars/infobar_overlay_request_config.h"
#import "ios/chrome/browser/overlays/model/public/default/default_infobar_overlay_request_config.h"
#import "ios/chrome/browser/overlays/model/public/overlay_request.h"
#import "ios/chrome/browser/overlays/model/public/overlay_request_support.h"
#import "ios/chrome/browser/overlays/model/public/overlay_response.h"
#import "ios/chrome/browser/shared/coordinator/layout_guide/layout_guide_util.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/ui/util/layout_guide_names.h"
#import "ios/chrome/browser/shared/ui/util/util_swift.h"
#import "ios/chrome/browser/ui/infobars/banners/infobar_banner_accessibility_util.h"
#import "ios/chrome/browser/ui/infobars/banners/infobar_banner_view_controller.h"
#import "ios/chrome/browser/ui/infobars/infobar_constants.h"
#import "ios/chrome/browser/ui/infobars/presentation/infobar_banner_positioner.h"
#import "ios/chrome/browser/ui/infobars/presentation/infobar_banner_transition_driver.h"
#import "ios/chrome/browser/overlays/ui_bundled/infobar_banner/autofill_address_profile/save_address_profile_infobar_banner_overlay_mediator.h"
#import "ios/chrome/browser/overlays/ui_bundled/infobar_banner/confirm/confirm_infobar_banner_overlay_mediator.h"
#import "ios/chrome/browser/overlays/ui_bundled/infobar_banner/features.h"
#import "ios/chrome/browser/overlays/ui_bundled/infobar_banner/infobar_banner_overlay_mediator.h"
#import "ios/chrome/browser/overlays/ui_bundled/infobar_banner/parcel_tracking/parcel_tracking_infobar_banner_overlay_mediator.h"
#import "ios/chrome/browser/overlays/ui_bundled/infobar_banner/passwords/password_infobar_banner_overlay_mediator.h"
#import "ios/chrome/browser/overlays/ui_bundled/infobar_banner/permissions/permissions_infobar_banner_overlay_mediator.h"
#import "ios/chrome/browser/overlays/ui_bundled/infobar_banner/safe_browsing/enhanced_safe_browsing_infobar_overlay_mediator.h"
#import "ios/chrome/browser/overlays/ui_bundled/infobar_banner/save_card/save_card_infobar_banner_overlay_mediator.h"
#import "ios/chrome/browser/overlays/ui_bundled/infobar_banner/sync_error/sync_error_infobar_banner_overlay_mediator.h"
#import "ios/chrome/browser/overlays/ui_bundled/infobar_banner/tailored_security/tailored_security_infobar_banner_overlay_mediator.h"
#import "ios/chrome/browser/overlays/ui_bundled/infobar_banner/translate/translate_infobar_banner_overlay_mediator.h"
#import "ios/chrome/browser/overlays/ui_bundled/overlay_request_coordinator+subclassing.h"
#import "ios/chrome/browser/overlays/ui_bundled/overlay_request_coordinator_delegate.h"
#import "ios/chrome/browser/overlays/ui_bundled/overlay_request_mediator_util.h"

@interface InfobarBannerOverlayCoordinator () <InfobarBannerPositioner>
// The list of supported mediator classes.
@property(class, nonatomic, readonly) NSArray<Class>* supportedMediatorClasses;
// The banner view being managed by this coordinator.
@property(nonatomic, strong) InfobarBannerViewController* bannerViewController;
// The transition delegate used by the coordinator to present the banner.
@property(nonatomic, strong)
    InfobarBannerTransitionDriver* bannerTransitionDriver;
@end

@implementation InfobarBannerOverlayCoordinator

#pragma mark - Accessors

+ (NSArray<Class>*)supportedMediatorClasses {
  return @[
    [PasswordInfobarBannerOverlayMediator class],
    [ConfirmInfobarBannerOverlayMediator class],
    [TranslateInfobarBannerOverlayMediator class],
    [SaveCardInfobarBannerOverlayMediator class],
    [SaveAddressProfileInfobarBannerOverlayMediator class],
    [PermissionsBannerOverlayMediator class],
    [TailoredSecurityInfobarBannerOverlayMediator class],
    [SyncErrorInfobarBannerOverlayMediator class],
    [ParcelTrackingBannerOverlayMediator class],
    [EnhancedSafeBrowsingBannerOverlayMediator class],
  ];
}

+ (const OverlayRequestSupport*)requestSupport {
  static std::unique_ptr<const OverlayRequestSupport> _requestSupport;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    _requestSupport =
        CreateAggregateSupportForMediators(self.supportedMediatorClasses);
  });
  return _requestSupport.get();
}

#pragma mark - InfobarBannerPositioner

- (CGFloat)bannerYPosition {
  LayoutGuideCenter* layoutGuideCenter =
      LayoutGuideCenterForBrowser(self.browser);
  UIView* topOmnibox =
      [layoutGuideCenter referencedViewUnderName:kTopOmniboxGuide];
  CGRect omniboxFrame = [topOmnibox convertRect:topOmnibox.bounds toView:nil];
  CGFloat omniboxMaxY = CGRectGetMaxY(omniboxFrame);

  // Use the top toolbar's layout guide when the omnibox is at the bottom.
  if (topOmnibox.hidden) {
    UIView* topToolbar =
        [layoutGuideCenter referencedViewUnderName:kPrimaryToolbarGuide];
    CGRect topToolbarFrame = [topToolbar convertRect:topToolbar.bounds
                                              toView:nil];
    CGFloat topToolbarMaxY =
        CGRectGetMaxY(topToolbarFrame) + kInfobarTopPaddingBottomOmnibox;
    return topToolbarMaxY;
  }
  return omniboxMaxY;
}

- (UIView*)bannerView {
  return self.bannerViewController.view;
}

#pragma mark - OverlayRequestCoordinator

- (void)startAnimated:(BOOL)animated {
  if (self.started || !self.request)
    return;
  // Create the mediator and use it aas the delegate for the banner view.
  InfobarOverlayRequestConfig* config =
      self.request->GetConfig<InfobarOverlayRequestConfig>();
  InfobarBannerOverlayMediator* mediator = [self newMediator];
  self.bannerViewController = [[InfobarBannerViewController alloc]
      initWithDelegate:mediator
         presentsModal:config->has_badge()
                  type:config->infobar_type()];
  mediator.consumer = self.bannerViewController;
  mediator.engagementTracker =
      feature_engagement::TrackerFactory::GetForBrowserState(
          self.browser->GetBrowserState());

  self.mediator = mediator;
  // Present the banner.
  self.bannerViewController.modalPresentationStyle = UIModalPresentationCustom;
  self.bannerTransitionDriver = [[InfobarBannerTransitionDriver alloc] init];
  self.bannerTransitionDriver.bannerPositioner = self;
  self.bannerViewController.transitioningDelegate = self.bannerTransitionDriver;
  self.bannerViewController.interactionDelegate = self.bannerTransitionDriver;
  __weak InfobarBannerOverlayCoordinator* weakSelf = self;
  [self.baseViewController
      presentViewController:self.viewController
                   animated:animated
                 completion:^{
                   InfobarBannerOverlayCoordinator* strongSelf = weakSelf;
                   if (strongSelf && strongSelf.started) {
                     [strongSelf finishPresentation];
                   }
                 }];
  self.started = YES;

  if (!UIAccessibilityIsVoiceOverRunning()) {
    // Auto-dismiss the banner after timeout if VoiceOver is off (banner should
    // persist until user explicitly swipes it away).
    [self performSelector:@selector(dismissBannerIfReady)
               withObject:nil
               afterDelay:[self infobarDuration].InSecondsF()];
  }
}

- (void)stopAnimated:(BOOL)animated {
  if (!self.started)
    return;
  // Mark started as NO before calling dismissal callback to prevent dup
  // stopAnimated: executions.
  self.started = NO;
  __weak InfobarBannerOverlayCoordinator* weakSelf = self;
  [self.baseViewController dismissViewControllerAnimated:animated
                                              completion:^{
                                                [weakSelf finishDismissal];
                                              }];
}

- (UIViewController*)viewController {
  return self.bannerViewController;
}

#pragma mark - Private

// Called when the presentation of the banner UI is completed.
- (void)finishPresentation {
  // Notify the presentation context that the presentation has finished.  This
  // is necessary to synchronize OverlayPresenter scheduling logic with the UI
  // layer.
  if (self.delegate) {
    self.delegate->OverlayUIDidFinishPresentation(self.request);
  }
  UpdateBannerAccessibilityForPresentation(self.baseViewController,
                                           self.viewController.view);
}

// Called when the dismissal of the banner UI is finished.
- (void)finishDismissal {
  InfobarBannerOverlayMediator* mediator =
      base::apple::ObjCCast<InfobarBannerOverlayMediator>(self.mediator);
  [mediator finishDismissal];
  self.bannerViewController = nil;
  self.mediator = nil;
  // Notify the presentation context that the dismissal has finished.  This
  // is necessary to synchronize OverlayPresenter scheduling logic with the UI
  // layer.
  if (self.delegate) {
    self.delegate->OverlayUIDidFinishDismissal(self.request);
  }
  UpdateBannerAccessibilityForDismissal(self.baseViewController);
}

// Creates a mediator instance from the supported mediator class list that
// supports the coordinator's request.
- (InfobarBannerOverlayMediator*)newMediator {
  if (DefaultInfobarOverlayRequestConfig::RequestSupport()->IsRequestSupported(
          self.request)) {
    DefaultInfobarOverlayRequestConfig* config =
        self.request->GetConfig<DefaultInfobarOverlayRequestConfig>();
    return [self mediatorForInfobarType:config->infobar_type()];
  }

  InfobarBannerOverlayMediator* mediator =
      base::apple::ObjCCast<InfobarBannerOverlayMediator>(GetMediatorForRequest(
          [self class].supportedMediatorClasses, self.request));
  DCHECK(mediator) << "None of the supported mediator classes support request.";
  return mediator;
}

// Returns the mediator corresponding to the given `infobarType`.
- (InfobarBannerOverlayMediator*)mediatorForInfobarType:
    (InfobarType)infobarType {
  Class mediatorClass = nil;

  switch (infobarType) {
    case InfobarType::kInfobarTypePasswordSave:
    case InfobarType::kInfobarTypePasswordUpdate:
      mediatorClass = [PasswordInfobarBannerOverlayMediator class];
      break;
    case InfobarType::kInfobarTypePermissions:
      mediatorClass = [PermissionsBannerOverlayMediator class];
      break;
    case InfobarType::kInfobarTypeTailoredSecurityService:
      mediatorClass = [TailoredSecurityInfobarBannerOverlayMediator class];
      break;
    case InfobarType::kInfobarTypeSaveCard:
      mediatorClass = [SaveCardInfobarBannerOverlayMediator class];
      break;
    case InfobarType::kInfobarTypeSyncError:
      mediatorClass = [SyncErrorInfobarBannerOverlayMediator class];
      break;
    case InfobarType::kInfobarTypeTranslate:
      mediatorClass = [TranslateInfobarBannerOverlayMediator class];
      break;
    case InfobarType::kInfobarTypeParcelTracking:
      mediatorClass = [ParcelTrackingBannerOverlayMediator class];
      break;
    case InfobarType::kInfobarTypeEnhancedSafeBrowsing:
      mediatorClass = [EnhancedSafeBrowsingBannerOverlayMediator class];
      break;
    default:
      NOTREACHED() << "Received unsupported infobarType.";
  }

  return [[mediatorClass alloc] initWithRequest:self.request];
}

// Indicate to the UI to dismiss itself if it is ready (e.g. the user is not
// currently interaction with it).
- (void)dismissBannerIfReady {
  [self.bannerViewController dismissWhenInteractionIsFinished];
}

// Determines the duration for which to show the infobar based on its priority
// and its type.
- (base::TimeDelta)infobarDuration {
  InfobarOverlayRequestConfig* config =
      self.request->GetConfig<InfobarOverlayRequestConfig>();

  // Experiments with longer infobar duration for passwords, cards and
  // addresses.
  InfobarType type = config->infobar_type();
  if (type == InfobarType::kInfobarTypePasswordSave ||
      type == InfobarType::kInfobarTypePasswordUpdate) {
    if (base::FeatureList::IsEnabled(kPasswordInfobarDisplayLength)) {
      return base::Seconds(kPasswordInfobarDisplayLengthParam.Get());
    }
  } else if (type == InfobarType::kInfobarTypeSaveCard) {
    if (base::FeatureList::IsEnabled(kCreditCardInfobarDisplayLength)) {
      return base::Seconds(kCreditCardInfobarDisplayLengthParam.Get());
    }
  } else if (type == InfobarType::kInfobarTypeSaveAutofillAddressProfile) {
    if (base::FeatureList::IsEnabled(kAddressInfobarDisplayLength)) {
      return base::Seconds(kAddressInfobarDisplayLengthParam.Get());
    }
  }

  return config->is_high_priority() ? kInfobarBannerLongPresentationDuration
                                    : kInfobarBannerDefaultPresentationDuration;
}

@end