chromium/ios/chrome/browser/overlays/ui_bundled/infobar_modal/translate/translate_infobar_modal_overlay_mediator.mm

// Copyright 2020 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_modal/translate/translate_infobar_modal_overlay_mediator.h"

#import "base/metrics/histogram_macros.h"
#import "base/metrics/sparse_histogram.h"
#import "base/strings/sys_string_conversions.h"
#import "components/metrics/metrics_log.h"
#import "components/translate/core/browser/translate_infobar_delegate.h"
#import "components/translate/core/browser/translate_step.h"
#import "components/translate/core/common/translate_metrics.h"
#import "components/translate/core/common/translate_util.h"
#import "ios/chrome/browser/infobars/model/overlays/infobar_overlay_util.h"
#import "ios/chrome/browser/overlays/model/public/default/default_infobar_overlay_request_config.h"
#import "ios/chrome/browser/shared/ui/list_model/list_model.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_text_item.h"
#import "ios/chrome/browser/translate/model/translate_infobar_metrics_recorder.h"
#import "ios/chrome/browser/overlays/ui_bundled/overlay_request_mediator+subclassing.h"

namespace {
const int kInvalidLanguageIndex = -1;
}  // namespace

@interface TranslateInfobarModalOverlayMediator ()

// The translate modal config from the request.
@property(nonatomic, readonly) DefaultInfobarOverlayRequestConfig* config;
// Holds the new source language selected by the user. kInvalidLanguageIndex if
// the user has not made any such selection.
@property(nonatomic, assign) int newSourceLanguageIndex;
// Whether the source language (initial or selected) is unknown.
@property(nonatomic, assign) BOOL sourceLanguageIsUnknown;
// Whether the source language was initially unknown.
@property(nonatomic, assign) BOOL sourceLanguageIsInitiallyUnknown;

// Holds the new target language selected by the user. kInvalidLanguageIndex if
// the user has not made any such selection.
@property(nonatomic, assign) int newTargetLanguageIndex;

// Maps the index from the source language selection view to
// `config->language_names()`.
@property(nonatomic, assign) std::vector<int> sourceLanguageMapping;

// Maps the index from the target language selection view to
// `config->language_names()`.
@property(nonatomic, assign) std::vector<int> targetLanguageMapping;

// Supported language names.
@property(nonatomic, assign) std::vector<std::u16string> languageNames;

@end

@implementation TranslateInfobarModalOverlayMediator

#pragma mark - Accessors

- (DefaultInfobarOverlayRequestConfig*)config {
  return self.request
             ? self.request->GetConfig<DefaultInfobarOverlayRequestConfig>()
             : nullptr;
}

// Returns the delegate attached to the config.
- (translate::TranslateInfoBarDelegate*)translateDelegate {
  return static_cast<translate::TranslateInfoBarDelegate*>(
      self.config->delegate());
}

- (void)setConsumer:(id<InfobarTranslateModalConsumer>)consumer {
  _consumer = consumer;
  translate::TranslateInfoBarDelegate* delegate = self.translateDelegate;

  std::vector<std::u16string> languageNames;
  for (size_t i = 0; i < delegate->num_languages(); ++i) {
    languageNames.push_back(delegate->language_name_at((int(i))));
  }
  self.languageNames = languageNames;

  // Since this is displaying a new Modal, any new source/target language state
  // should be reset.
  self.newSourceLanguageIndex = kInvalidLanguageIndex;
  self.newTargetLanguageIndex = kInvalidLanguageIndex;
  self.sourceLanguageIsUnknown =
      delegate->unknown_language_name() == delegate->source_language_name();
  self.sourceLanguageIsInitiallyUnknown =
      delegate->unknown_language_name() ==
      delegate->initial_source_language_name();

  // The Translate button should be enabled whenever the page is untranslated,
  // which may be before any translation has been triggered or after an error
  // caused translation to fail.
  const translate::TranslateStep currentStep = delegate->translate_step();
  const BOOL currentStepUntranslated =
      currentStep ==
          translate::TranslateStep::TRANSLATE_STEP_BEFORE_TRANSLATE ||
      currentStep == translate::TranslateStep::TRANSLATE_STEP_TRANSLATE_ERROR;

  [self.consumer
      setupModalViewControllerWithPrefs:
          [self createPrefDictionaryForSourceLanguage:
                    base::SysUTF16ToNSString(delegate->source_language_name())
                                       targetLanguage:
                                           base::SysUTF16ToNSString(
                                               delegate->target_language_name())
                               translateButtonEnabled:currentStepUntranslated]];
}

- (void)setSourceLanguageSelectionConsumer:
    (id<InfobarTranslateLanguageSelectionConsumer>)
        sourceLanguageSelectionConsumer {
  _sourceLanguageSelectionConsumer = sourceLanguageSelectionConsumer;
  NSArray<TableViewTextItem*>* items =
      [self loadTranslateLanguageItemsForSelectingLanguage:YES];
  [self.sourceLanguageSelectionConsumer setTranslateLanguageItems:items];
}

- (void)setTargetLanguageSelectionConsumer:
    (id<InfobarTranslateLanguageSelectionConsumer>)
        targetLanguageSelectionConsumer {
  _targetLanguageSelectionConsumer = targetLanguageSelectionConsumer;
  NSArray<TableViewTextItem*>* items =
      [self loadTranslateLanguageItemsForSelectingLanguage:NO];
  [self.targetLanguageSelectionConsumer setTranslateLanguageItems:items];
}

#pragma mark - OverlayRequestMediator

+ (const OverlayRequestSupport*)requestSupport {
  return DefaultInfobarOverlayRequestConfig::RequestSupport();
}

#pragma mark InfobarModalDelegate

- (void)modalInfobarButtonWasAccepted:(id)infobarModal {
  [self startTranslation];

  [self dismissOverlay];
}

#pragma mark - InfobarTranslateModalDelegate

- (void)showSourceLanguage {
  translate::ReportCompactInfobarEvent(translate::InfobarEvent::INFOBAR_REVERT);

  InfoBarIOS* infobar = GetOverlayRequestInfobar(self.request);
  self.translateDelegate->RevertWithoutClosingInfobar();
  infobar->set_accepted(false);
  [TranslateInfobarMetricsRecorder
      recordModalEvent:MobileMessagesTranslateModalEvent::ShowOriginal];

  [self dismissOverlay];
}

- (void)translateWithNewLanguages {
  [self updateLanguagesIfNecessary];
  translate::ReportCompactInfobarEvent(
      translate::InfobarEvent::INFOBAR_TARGET_TAB_TRANSLATE);

  [self startTranslation];

  [self dismissOverlay];
}

- (void)showChangeSourceLanguageOptions {
  translate::ReportCompactInfobarEvent(
      translate::InfobarEvent::INFOBAR_PAGE_NOT_IN);
  [TranslateInfobarMetricsRecorder
      recordModalEvent:MobileMessagesTranslateModalEvent::ChangeSourceLanguage];

  [self.translateMediatorDelegate showChangeSourceLanguageOptions];
}

- (void)showChangeTargetLanguageOptions {
  translate::ReportCompactInfobarEvent(
      translate::InfobarEvent::INFOBAR_MORE_LANGUAGES);
  [TranslateInfobarMetricsRecorder
      recordModalEvent:MobileMessagesTranslateModalEvent::ChangeTargetLanguage];

  [self.translateMediatorDelegate showChangeTargetLanguageOptions];
}

- (void)alwaysTranslateSourceLanguage {
  translate::ReportCompactInfobarEvent(
      translate::InfobarEvent::INFOBAR_ALWAYS_TRANSLATE);
  [TranslateInfobarMetricsRecorder
      recordModalEvent:MobileMessagesTranslateModalEvent::
                           TappedAlwaysTranslate];

  [self toggleAlwaysTranslate];

  // Since toggle turned on always translate, translate now if not already
  // translated.
  if (self.translateDelegate->translate_step() ==
      translate::TranslateStep::TRANSLATE_STEP_BEFORE_TRANSLATE) {
    [self startTranslation];
  }

  [self dismissOverlay];
}

- (void)undoAlwaysTranslateSourceLanguage {
  DCHECK(self.translateDelegate->IsTranslatableLanguageByPrefs());
  translate::ReportCompactInfobarEvent(
      translate::InfobarEvent::INFOBAR_ALWAYS_TRANSLATE_UNDO);
  [self toggleAlwaysTranslate];

  [self dismissOverlay];
}

- (void)neverTranslateSourceLanguage {
  DCHECK(self.translateDelegate->IsTranslatableLanguageByPrefs());
  translate::ReportCompactInfobarEvent(
      translate::InfobarEvent::INFOBAR_NEVER_TRANSLATE);
  [TranslateInfobarMetricsRecorder
      recordModalEvent:MobileMessagesTranslateModalEvent::
                           TappedNeverForSourceLanguage];
  [self ToggleNeverTranslateSourceLanguage];

  [self dismissOverlay];
}

- (void)undoNeverTranslateSourceLanguage {
  DCHECK(!self.translateDelegate->IsTranslatableLanguageByPrefs());
  [self ToggleNeverTranslateSourceLanguage];

  [self dismissOverlay];
}

- (void)neverTranslateSite {
  DCHECK(!self.translateDelegate->IsSiteOnNeverPromptList());
  translate::ReportCompactInfobarEvent(
      translate::InfobarEvent::INFOBAR_NEVER_TRANSLATE_SITE);
  [TranslateInfobarMetricsRecorder
      recordModalEvent:MobileMessagesTranslateModalEvent::
                           TappedNeverForThisSite];
  [self toggleNeverTranslateSite];

  [self dismissOverlay];
}

- (void)undoNeverTranslateSite {
  DCHECK(self.translateDelegate->IsSiteOnNeverPromptList());
  [self toggleNeverTranslateSite];

  [self dismissOverlay];
}

#pragma mark - InfobarTranslateLanguageSelectionDelegate

- (void)didSelectSourceLanguageIndex:(int)itemIndex
                            withName:(NSString*)languageName {
  translate::TranslateInfoBarDelegate* delegate = self.translateDelegate;
  int languageIndex = self.sourceLanguageMapping[itemIndex];
  std::vector<std::u16string> languageNames = self.languageNames;

  // Sanity check that `languageIndex` matches the languageName selected.
  DCHECK([languageName isEqualToString:base::SysUTF16ToNSString(
                                           languageNames.at(languageIndex))]);

  self.newSourceLanguageIndex = languageIndex;
  std::u16string sourceLanguage = languageNames.at(languageIndex);

  std::u16string targetLanguage = delegate->target_language_name();
  if (self.newTargetLanguageIndex != kInvalidLanguageIndex) {
    targetLanguage = languageNames.at(self.newTargetLanguageIndex);
  }
  self.sourceLanguageIsUnknown =
      sourceLanguage == delegate->unknown_language_name();
  [self.consumer
      setupModalViewControllerWithPrefs:
          [self createPrefDictionaryForSourceLanguage:base::SysUTF16ToNSString(
                                                          sourceLanguage)
                                       targetLanguage:base::SysUTF16ToNSString(
                                                          targetLanguage)
                               translateButtonEnabled:YES]];
}

- (void)didSelectTargetLanguageIndex:(int)itemIndex
                            withName:(NSString*)languageName {
  translate::TranslateInfoBarDelegate* delegate = self.translateDelegate;
  int languageIndex = self.targetLanguageMapping[itemIndex];
  std::vector<std::u16string> languageNames = self.languageNames;

  // Sanity check that `languageIndex` matches the languageName selected.
  DCHECK([languageName isEqualToString:base::SysUTF16ToNSString(
                                           languageNames.at(languageIndex))]);

  self.newTargetLanguageIndex = languageIndex;
  std::u16string targetLanguage = languageNames.at(languageIndex);

  std::u16string sourceLanguage = delegate->source_language_name();
  if (self.newSourceLanguageIndex != kInvalidLanguageIndex) {
    sourceLanguage = languageNames.at(self.newSourceLanguageIndex);
  }
  [self.consumer
      setupModalViewControllerWithPrefs:
          [self createPrefDictionaryForSourceLanguage:base::SysUTF16ToNSString(
                                                          sourceLanguage)
                                       targetLanguage:base::SysUTF16ToNSString(
                                                          targetLanguage)
                               translateButtonEnabled:YES]];
}

#pragma mark - Private

// Returns the language `items` to be displayed.
- (NSArray<TableViewTextItem*>*)loadTranslateLanguageItemsForSelectingLanguage:
    (BOOL)sourceLanguage {
  translate::TranslateInfoBarDelegate* delegate = self.translateDelegate;
  std::vector<std::u16string> languageNames = self.languageNames;

  // In the instance that the user has already selected a different source
  // language, then we should be using that language as the one to potentially
  // check or not show.
  std::u16string sourceLanguageName =
      self.newSourceLanguageIndex != kInvalidLanguageIndex
          ? languageNames.at(self.newSourceLanguageIndex)
          : delegate->source_language_name();
  // In the instance that the user has already selected a different target
  // language, then we should be using that language as the one to potentially
  // check or not show.
  std::u16string targetLanguageName =
      self.newTargetLanguageIndex != kInvalidLanguageIndex
          ? languageNames.at(self.newTargetLanguageIndex)
          : delegate->target_language_name();

  BOOL shouldSkipFirstLanguage =
      !(sourceLanguage && self.sourceLanguageIsInitiallyUnknown);
  NSMutableArray<TableViewTextItem*>* items = [NSMutableArray array];
  std::vector<int> languageMapping;
  languageMapping.reserve(languageNames.size());

  for (size_t i = 0; i < languageNames.size(); ++i) {
    if (shouldSkipFirstLanguage && i == 0) {
      // "Detected Language" is the first item in the languages list and should
      // only be added to the source language menu.
      continue;
    }

    std::u16string languageName = languageNames.at((int)i);
    languageMapping.push_back(i);
    TableViewTextItem* item =
        [[TableViewTextItem alloc] initWithType:kItemTypeEnumZero];
    item.text = base::SysUTF16ToNSString(languageName);

    if (languageName == sourceLanguageName) {
      if (!sourceLanguage) {
        // Disable for source language if selecting the target
        // language to prevent same language translation. Need to add item,
        // because the row number needs to match language's index in
        // translateInfobarDelegate.
        item.enabled = NO;
      }
    }
    if (languageName == targetLanguageName) {
      if (sourceLanguage) {
        // Disable for target language if selecting the source
        // language to prevent same language translation. Need to add item,
        // because the row number needs to match language's index in
        // translateInfobarDelegate.
        item.enabled = NO;
      }
    }

    if ((sourceLanguage && sourceLanguageName == languageName) ||
        (!sourceLanguage && targetLanguageName == languageName)) {
      item.checked = YES;
    }
    [items addObject:item];
  }
  if (sourceLanguage) {
    self.sourceLanguageMapping = languageMapping;
  } else {
    self.targetLanguageMapping = languageMapping;
  }

  return items;
}

// Records a histogram of `histogram` for `langCode`. This is used to log the
// language distribution of certain Translate events.
- (void)recordLanguageDataHistogram:(const std::string&)histogramName
                       languageCode:(const std::string&)langCode {
  // TODO(crbug.com/40107868): Use function version of macros here and in
  // TranslateInfobarController.
  base::SparseHistogram::FactoryGet(
      histogramName, base::HistogramBase::kUmaTargetedHistogramFlag)
      ->Add(metrics::MetricsLog::Hash(langCode));
}

// Updates source and target languages if necessary.
- (void)updateLanguagesIfNecessary {
  int sourceLanguageIndex = self.newSourceLanguageIndex;
  int targetLanguageIndex = self.newTargetLanguageIndex;

  if (sourceLanguageIndex != kInvalidLanguageIndex ||
      targetLanguageIndex != kInvalidLanguageIndex) {
    translate::TranslateInfoBarDelegate* delegate = self.translateDelegate;

    if (sourceLanguageIndex != kInvalidLanguageIndex) {
      std::string sourceLanguageCode =
          delegate->language_code_at(sourceLanguageIndex);
      if (delegate->source_language_code() != sourceLanguageCode) {
        delegate->UpdateSourceLanguage(sourceLanguageCode);
      }
    }

    if (targetLanguageIndex != kInvalidLanguageIndex) {
      std::string targetLanguageCode =
          delegate->language_code_at(targetLanguageIndex);
      if (delegate->target_language_code() != targetLanguageCode) {
        delegate->UpdateTargetLanguage(targetLanguageCode);
      }
    }

    self.newSourceLanguageIndex = kInvalidLanguageIndex;
    self.newTargetLanguageIndex = kInvalidLanguageIndex;
  }
}

// Returns a dictionary of prefs to send to the modalConsumer depending on
// `sourceLanguage`, `targetLanguage`, `translateButtonEnabled`, and
// `self.currentStep`.
- (NSDictionary*)createPrefDictionaryForSourceLanguage:(NSString*)sourceLanguage
                                        targetLanguage:(NSString*)targetLanguage
                                translateButtonEnabled:
                                    (BOOL)translateButtonEnabled {
  translate::TranslateInfoBarDelegate* delegate = self.translateDelegate;
  const translate::TranslateStep currentStep = delegate->translate_step();

  // Modal state following a translate error should be the same as on an
  // untranslated page.
  BOOL currentStepUntranslated =
      currentStep ==
          translate::TranslateStep::TRANSLATE_STEP_BEFORE_TRANSLATE ||
      currentStep == translate::TranslateStep::TRANSLATE_STEP_TRANSLATE_ERROR;
  BOOL currentStepAfterTranslate =
      currentStep == translate::TranslateStep::TRANSLATE_STEP_AFTER_TRANSLATE;
  BOOL updateLanguageBeforeTranslate =
      self.newSourceLanguageIndex != kInvalidLanguageIndex ||
      self.newTargetLanguageIndex != kInvalidLanguageIndex;

  return @{
    kSourceLanguagePrefKey : sourceLanguage,
    kSourceLanguageIsUnknownPrefKey : @(self.sourceLanguageIsUnknown),
    kTargetLanguagePrefKey : targetLanguage,
    kEnableTranslateButtonPrefKey : @(translateButtonEnabled),
    kUpdateLanguageBeforeTranslatePrefKey : @(updateLanguageBeforeTranslate),
    kEnableAndDisplayShowOriginalButtonPrefKey : @(currentStepAfterTranslate),
    kShouldAlwaysTranslatePrefKey : @(delegate->ShouldAlwaysTranslate()),
    kDisplayNeverTranslateLanguagePrefKey : @(currentStepUntranslated),
    kDisplayNeverTranslateSiteButtonPrefKey : @(currentStepUntranslated),
    kIsTranslatableLanguagePrefKey :
        @(delegate->IsTranslatableLanguageByPrefs()),
    kIsSiteOnNeverPromptListPrefKey : @(delegate->IsSiteOnNeverPromptList()),
  };
}

// Called when the always translate preference has been toggled.
- (void)toggleAlwaysTranslate {
  translate::TranslateInfoBarDelegate* delegate = self.translateDelegate;
  const bool enablingAlwaysTranslate = !delegate->ShouldAlwaysTranslate();
  delegate->ToggleAlwaysTranslate();
  if (enablingAlwaysTranslate) {
    delegate->Translate();
  }
}

// Called when the never translate source language preference has been toggled.
- (void)ToggleNeverTranslateSourceLanguage {
  translate::TranslateInfoBarDelegate* delegate = self.translateDelegate;
  const bool shouldRemoveInfobar = delegate->IsTranslatableLanguageByPrefs();
  delegate->ToggleTranslatableLanguageByPrefs();
  // Remove infobar if turning it on.
  if (shouldRemoveInfobar) {
    InfoBarIOS* infobar = GetOverlayRequestInfobar(self.request);
    infobar->RemoveSelf();
  }
}

// Called when the never translate site preference has been toggled.
- (void)toggleNeverTranslateSite {
  translate::TranslateInfoBarDelegate* delegate = self.translateDelegate;
  const bool shouldRemoveInfobar = !delegate->IsSiteOnNeverPromptList();
  delegate->ToggleNeverPromptSite();
  // Remove infobar if turning it on.
  if (shouldRemoveInfobar) {
    InfoBarIOS* infobar = GetOverlayRequestInfobar(self.request);
    infobar->RemoveSelf();
  }
}

// Starts translation.
- (void)startTranslation {
  translate::TranslateInfoBarDelegate* delegate = self.translateDelegate;
  if (delegate->ShouldAutoAlwaysTranslate()) {
    delegate->ToggleAlwaysTranslate();
  }
  delegate->Translate();
}

@end