blob: 93e4ec3ce640d4d1c4ce131e7719d9356cf28bbc [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/translate/translate_infobar_controller.h"
#import <UIKit/UIKit.h>
#include <stddef.h>
#include <memory>
#include "base/logging.h"
#include "base/mac/foundation_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/sparse_histogram.h"
#include "base/optional.h"
#include "base/strings/sys_string_conversions.h"
#include "components/metrics/metrics_log.h"
#include "components/strings/grit/components_strings.h"
#include "components/translate/core/browser/translate_infobar_delegate.h"
#import "ios/chrome/browser/infobars/infobar_controller+protected.h"
#include "ios/chrome/browser/infobars/infobar_controller_delegate.h"
#include "ios/chrome/browser/translate/language_selection_context.h"
#include "ios/chrome/browser/translate/language_selection_delegate.h"
#include "ios/chrome/browser/translate/language_selection_handler.h"
#import "ios/chrome/browser/translate/translate_infobar_delegate_observer_bridge.h"
#include "ios/chrome/browser/translate/translate_option_selection_delegate.h"
#include "ios/chrome/browser/translate/translate_option_selection_handler.h"
#import "ios/chrome/browser/ui/translate/translate_infobar_view.h"
#import "ios/chrome/browser/ui/translate/translate_infobar_view_delegate.h"
#import "ios/chrome/browser/ui/translate/translate_notification_delegate.h"
#import "ios/chrome/browser/ui/translate/translate_notification_handler.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/image/image.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
// Whether langugage selection popup menu is offered; and whether it is to
// select the source or the target language.
typedef NS_ENUM(NSInteger, LanguageSelectionState) {
LanguageSelectionStateNone,
LanguageSelectionStateSource,
LanguageSelectionStateTarget,
};
// Various user actions to keep track of.
typedef NS_OPTIONS(NSUInteger, UserAction) {
UserActionNone = 0,
UserActionTranslate = 1 << 0,
UserActionRevert = 1 << 1,
UserActionAlwaysTranslate = 1 << 2,
UserActionNeverTranslateLanguage = 1 << 3,
UserActionNeverTranslateSite = 1 << 4,
UserActionExpandMenu = 1 << 5,
};
// UMA histogram names.
// Note: These string constants are repeated in TranslateCompactInfoBar.java.
const char kLanguageHistogramTranslate[] =
"Translate.CompactInfobar.Language.Translate";
const char kLanguageHistogramMoreLanguages[] =
"Translate.CompactInfobar.Language.MoreLanguages";
const char kLanguageHistogramPageNotInLanguage[] =
"Translate.CompactInfobar.Language.PageNotIn";
const char kLanguageHistogramAlwaysTranslate[] =
"Translate.CompactInfobar.Language.AlwaysTranslate";
const char kLanguageHistogramNeverTranslate[] =
"Translate.CompactInfobar.Language.NeverTranslate";
const char kEventHistogram[] = "Translate.CompactInfobar.Event";
const char kTranslationCountHistogram[] =
"Translate.CompactInfobar.TranslationsPerPage";
// Enum for the Translate.CompactInfobar.Event UMA histogram.
// Note: These values are repeated as constants in TranslateCompactInfoBar.java.
// Note: This enum is used to back an UMA histogram, and should be treated as
// append-only.
// TODO(crbug.com/933371): Share these enums with Java.
enum class InfobarEvent {
INFOBAR_IMPRESSION = 0,
INFOBAR_TARGET_TAB_TRANSLATE = 1,
INFOBAR_DECLINE = 2,
INFOBAR_OPTIONS = 3,
INFOBAR_MORE_LANGUAGES = 4,
INFOBAR_MORE_LANGUAGES_TRANSLATE = 5,
INFOBAR_PAGE_NOT_IN = 6,
INFOBAR_ALWAYS_TRANSLATE = 7,
INFOBAR_NEVER_TRANSLATE = 8,
INFOBAR_NEVER_TRANSLATE_SITE = 9,
INFOBAR_SCROLL_HIDE = 10,
INFOBAR_SCROLL_SHOW = 11,
INFOBAR_REVERT = 12,
INFOBAR_SNACKBAR_ALWAYS_TRANSLATE_IMPRESSION = 13,
INFOBAR_SNACKBAR_NEVER_TRANSLATE_IMPRESSION = 14,
INFOBAR_SNACKBAR_NEVER_TRANSLATE_SITE_IMPRESSION = 15,
INFOBAR_SNACKBAR_CANCEL_ALWAYS = 16,
INFOBAR_SNACKBAR_CANCEL_NEVER_SITE = 17,
INFOBAR_SNACKBAR_CANCEL_NEVER = 18,
INFOBAR_ALWAYS_TRANSLATE_UNDO = 19,
INFOBAR_CLOSE_DEPRECATED = 20,
INFOBAR_SNACKBAR_AUTO_ALWAYS_IMPRESSION = 21,
INFOBAR_SNACKBAR_AUTO_NEVER_IMPRESSION = 22,
INFOBAR_SNACKBAR_CANCEL_AUTO_ALWAYS = 23,
INFOBAR_SNACKBAR_CANCEL_AUTO_NEVER = 24,
INFOBAR_HISTOGRAM_BOUNDARY = 25,
kMaxValue = INFOBAR_HISTOGRAM_BOUNDARY,
};
} // namespace
@interface TranslateInfoBarController () <LanguageSelectionDelegate,
TranslateInfobarDelegateObserving,
TranslateInfobarViewDelegate,
TranslateNotificationDelegate,
TranslateOptionSelectionDelegate> {
std::unique_ptr<TranslateInfobarDelegateObserverBridge>
_translateInfobarDelegateObserver;
}
// Overrides superclass property.
@property(nonatomic, readonly)
translate::TranslateInfoBarDelegate* infoBarDelegate;
@property(nonatomic, weak) TranslateInfobarView* infobarView;
// Indicates whether langugage selection popup menu is offered; and whether it
// is to select the source or the target language.
@property(nonatomic, assign) LanguageSelectionState languageSelectionState;
// Tracks user actions.
@property(nonatomic, assign) UserAction userAction;
// Tracks the total number of translations in a page, incl. reverts to original.
@property(nonatomic, assign) NSUInteger translationsCount;
@end
@implementation TranslateInfoBarController
@dynamic infoBarDelegate;
#pragma mark - InfoBarControllerProtocol
- (instancetype)initWithInfoBarDelegate:
(translate::TranslateInfoBarDelegate*)infoBarDelegate {
self = [super initWithInfoBarDelegate:infoBarDelegate];
if (self) {
_translateInfobarDelegateObserver =
std::make_unique<TranslateInfobarDelegateObserverBridge>(
infoBarDelegate, self);
_userAction = UserActionNone;
[self recordInfobarEvent:InfobarEvent::INFOBAR_IMPRESSION];
}
return self;
}
- (UIView*)infobarView {
TranslateInfobarView* infobarView =
[[TranslateInfobarView alloc] initWithFrame:CGRectZero];
infobarView.sourceLanguage = self.sourceLanguage;
infobarView.targetLanguage = self.targetLanguage;
infobarView.delegate = self;
infobarView.state =
[self translateInfobarViewStateForTranslateStep:self.infoBarDelegate
->translate_step()];
_infobarView = infobarView;
return infobarView;
}
#pragma mark - TranslateInfobarDelegateObserving
- (void)translateInfoBarDelegate:(translate::TranslateInfoBarDelegate*)delegate
didChangeTranslateStep:(translate::TranslateStep)step
withErrorType:(translate::TranslateErrors::Type)errorType {
_infobarView.state = [self translateInfobarViewStateForTranslateStep:step];
if (step == translate::TranslateStep::TRANSLATE_STEP_TRANSLATE_ERROR) {
[self.translateNotificationHandler
showTranslateNotificationWithDelegate:self
notificationType:TranslateNotificationTypeError];
}
if (step == translate::TranslateStep::TRANSLATE_STEP_TRANSLATE_ERROR ||
step == translate::TranslateStep::TRANSLATE_STEP_AFTER_TRANSLATE) {
[self incrementAndRecordTranslationsCount];
}
}
- (BOOL)translateInfoBarDelegateDidDismissWithoutInteraction:
(translate::TranslateInfoBarDelegate*)delegate {
return self.userAction == UserActionNone;
}
#pragma mark - TranslateInfobarViewDelegate
- (void)translateInfobarViewDidTapSourceLangugage:
(TranslateInfobarView*)sender {
if ([self shouldIgnoreUserInteraction])
return;
self.userAction |= UserActionRevert;
[self recordInfobarEvent:InfobarEvent::INFOBAR_REVERT];
[self incrementAndRecordTranslationsCount];
self.infoBarDelegate->RevertWithoutClosingInfobar();
_infobarView.state = TranslateInfobarViewStateBeforeTranslate;
}
- (void)translateInfobarViewDidTapTargetLangugage:
(TranslateInfobarView*)sender {
if ([self shouldIgnoreUserInteraction])
return;
self.userAction |= UserActionTranslate;
[self recordInfobarEvent:InfobarEvent::INFOBAR_TARGET_TAB_TRANSLATE];
[self
recordLanguageDataHistogram:kLanguageHistogramTranslate
languageCode:self.infoBarDelegate->target_language_code()];
if (self.infoBarDelegate->ShouldAutoAlwaysTranslate()) {
[self recordInfobarEvent:InfobarEvent::
INFOBAR_SNACKBAR_AUTO_ALWAYS_IMPRESSION];
// Page will be translated once the snackbar finishes showing.
[self.translateNotificationHandler
showTranslateNotificationWithDelegate:self
notificationType:
TranslateNotificationTypeAutoAlwaysTranslate];
} else {
self.infoBarDelegate->Translate();
}
}
- (void)translateInfobarViewDidTapOptions:(TranslateInfobarView*)sender {
if ([self shouldIgnoreUserInteraction])
return;
self.userAction |= UserActionExpandMenu;
[self recordInfobarEvent:InfobarEvent::INFOBAR_OPTIONS];
[self showTranslateOptionSelector];
}
- (void)translateInfobarViewDidTapDismiss:(TranslateInfobarView*)sender {
if ([self shouldIgnoreUserInteraction])
return;
self.infoBarDelegate->InfoBarDismissed();
if (self.userAction == UserActionNone) {
[self recordInfobarEvent:InfobarEvent::INFOBAR_DECLINE];
}
if (self.infoBarDelegate->ShouldAutoNeverTranslate()) {
[self recordInfobarEvent:InfobarEvent::
INFOBAR_SNACKBAR_AUTO_NEVER_IMPRESSION];
// Infobar will dismiss once the snackbar finishes showing.
[self.translateNotificationHandler
showTranslateNotificationWithDelegate:self
notificationType:
TranslateNotificationTypeAutoNeverTranslate];
} else {
self.delegate->RemoveInfoBar();
}
}
#pragma mark - LanguageSelectionDelegate
- (void)languageSelectorSelectedLanguage:(std::string)languageCode {
if (self.languageSelectionState == LanguageSelectionStateSource) {
[self recordLanguageDataHistogram:kLanguageHistogramPageNotInLanguage
languageCode:languageCode];
self.infoBarDelegate->UpdateOriginalLanguage(languageCode);
_infobarView.sourceLanguage = self.sourceLanguage;
} else {
[self recordInfobarEvent:InfobarEvent::INFOBAR_MORE_LANGUAGES_TRANSLATE];
[self recordLanguageDataHistogram:kLanguageHistogramMoreLanguages
languageCode:languageCode];
self.infoBarDelegate->UpdateTargetLanguage(languageCode);
_infobarView.targetLanguage = self.targetLanguage;
}
self.languageSelectionState = LanguageSelectionStateNone;
self.infoBarDelegate->Translate();
[_infobarView updateUIForPopUpMenuDisplayed:NO];
}
- (void)languageSelectorClosedWithoutSelection {
self.languageSelectionState = LanguageSelectionStateNone;
[_infobarView updateUIForPopUpMenuDisplayed:NO];
}
#pragma mark - TranslateOptionSelectionDelegate
- (void)popupMenuTableViewControllerDidSelectTargetLanguageSelector:
(PopupMenuTableViewController*)sender {
if ([self shouldIgnoreUserInteraction])
return;
self.userAction |= UserActionExpandMenu;
[self recordInfobarEvent:InfobarEvent::INFOBAR_MORE_LANGUAGES];
[_infobarView updateUIForPopUpMenuDisplayed:NO];
self.languageSelectionState = LanguageSelectionStateTarget;
[self showLanguageSelector];
}
- (void)popupMenuTableViewControllerDidSelectAlwaysTranslateSourceLanguage:
(PopupMenuTableViewController*)sender {
if ([self shouldIgnoreUserInteraction])
return;
self.userAction |= UserActionAlwaysTranslate;
[_infobarView updateUIForPopUpMenuDisplayed:NO];
if (self.infoBarDelegate->ShouldAlwaysTranslate()) {
[self recordInfobarEvent:InfobarEvent::INFOBAR_ALWAYS_TRANSLATE_UNDO];
self.infoBarDelegate->ToggleAlwaysTranslate();
} else {
[self recordInfobarEvent:InfobarEvent::INFOBAR_ALWAYS_TRANSLATE];
[self recordInfobarEvent:InfobarEvent::
INFOBAR_SNACKBAR_ALWAYS_TRANSLATE_IMPRESSION];
[self recordLanguageDataHistogram:kLanguageHistogramAlwaysTranslate
languageCode:self.infoBarDelegate
->original_language_code()];
// Page will be translated once the snackbar finishes showing.
[self.translateNotificationHandler
showTranslateNotificationWithDelegate:self
notificationType:
TranslateNotificationTypeAlwaysTranslate];
}
}
- (void)popupMenuTableViewControllerDidSelectNeverTranslateSourceLanguage:
(PopupMenuTableViewController*)sender {
if ([self shouldIgnoreUserInteraction])
return;
self.userAction |= UserActionNeverTranslateLanguage;
[_infobarView updateUIForPopUpMenuDisplayed:NO];
if (self.infoBarDelegate->IsTranslatableLanguageByPrefs()) {
[self recordInfobarEvent:InfobarEvent::INFOBAR_NEVER_TRANSLATE];
[self recordInfobarEvent:InfobarEvent::
INFOBAR_SNACKBAR_NEVER_TRANSLATE_IMPRESSION];
[self recordLanguageDataHistogram:kLanguageHistogramNeverTranslate
languageCode:self.infoBarDelegate
->original_language_code()];
// Infobar will dismiss once the snackbar finishes showing.
[self.translateNotificationHandler
showTranslateNotificationWithDelegate:self
notificationType:
TranslateNotificationTypeNeverTranslate];
}
}
- (void)popupMenuTableViewControllerDidSelectNeverTranslateSite:
(PopupMenuTableViewController*)sender {
if ([self shouldIgnoreUserInteraction])
return;
self.userAction |= UserActionNeverTranslateSite;
[_infobarView updateUIForPopUpMenuDisplayed:NO];
if (!self.infoBarDelegate->IsSiteBlacklisted()) {
[self recordInfobarEvent:InfobarEvent::INFOBAR_NEVER_TRANSLATE_SITE];
[self recordInfobarEvent:
InfobarEvent::INFOBAR_SNACKBAR_NEVER_TRANSLATE_SITE_IMPRESSION];
// Infobar will dismiss once the snackbar finishes showing.
[self.translateNotificationHandler
showTranslateNotificationWithDelegate:self
notificationType:
TranslateNotificationTypeNeverTranslateSite];
}
}
- (void)popupMenuTableViewControllerDidSelectSourceLanguageSelector:
(PopupMenuTableViewController*)sender {
if ([self shouldIgnoreUserInteraction])
return;
self.userAction |= UserActionExpandMenu;
[self recordInfobarEvent:InfobarEvent::INFOBAR_PAGE_NOT_IN];
[_infobarView updateUIForPopUpMenuDisplayed:NO];
self.languageSelectionState = LanguageSelectionStateSource;
[self showLanguageSelector];
}
- (void)popupMenuPresenterDidCloseWithoutSelection:(PopupMenuPresenter*)sender {
[_infobarView updateUIForPopUpMenuDisplayed:NO];
}
#pragma mark - TranslateNotificationDelegate
- (void)translateNotificationHandlerDidDismiss:
(id<TranslateNotificationHandler>)sender
notificationType:(TranslateNotificationType)type {
switch (type) {
case TranslateNotificationTypeAlwaysTranslate:
case TranslateNotificationTypeAutoAlwaysTranslate:
self.infoBarDelegate->ToggleAlwaysTranslate();
self.infoBarDelegate->Translate();
break;
case TranslateNotificationTypeNeverTranslate:
case TranslateNotificationTypeAutoNeverTranslate:
self.infoBarDelegate->ToggleTranslatableLanguageByPrefs();
self.delegate->RemoveInfoBar();
break;
case TranslateNotificationTypeNeverTranslateSite:
self.infoBarDelegate->ToggleSiteBlacklist();
self.delegate->RemoveInfoBar();
break;
case TranslateNotificationTypeError:
// No-op.
break;
}
}
- (void)translateNotificationHandlerDidUndo:
(id<TranslateNotificationHandler>)sender
notificationType:(TranslateNotificationType)type {
switch (type) {
case TranslateNotificationTypeAlwaysTranslate:
[self recordInfobarEvent:InfobarEvent::INFOBAR_SNACKBAR_CANCEL_ALWAYS];
break;
case TranslateNotificationTypeAutoAlwaysTranslate:
[self
recordInfobarEvent:InfobarEvent::INFOBAR_SNACKBAR_CANCEL_AUTO_ALWAYS];
break;
case TranslateNotificationTypeNeverTranslate:
[self recordInfobarEvent:InfobarEvent::INFOBAR_SNACKBAR_CANCEL_NEVER];
break;
case TranslateNotificationTypeAutoNeverTranslate:
[self
recordInfobarEvent:InfobarEvent::INFOBAR_SNACKBAR_CANCEL_AUTO_NEVER];
break;
case TranslateNotificationTypeNeverTranslateSite:
[self
recordInfobarEvent:InfobarEvent::INFOBAR_SNACKBAR_CANCEL_NEVER_SITE];
break;
case TranslateNotificationTypeError:
// No-op.
break;
}
}
- (NSString*)sourceLanguage {
return base::SysUTF16ToNSString(
self.infoBarDelegate->original_language_name());
}
- (NSString*)targetLanguage {
return base::SysUTF16ToNSString(self.infoBarDelegate->target_language_name());
}
#pragma mark - Private
// Returns the infobar view state for the given translate::TranslateStep.
- (TranslateInfobarViewState)translateInfobarViewStateForTranslateStep:
(translate::TranslateStep)step {
switch (step) {
case translate::TranslateStep::TRANSLATE_STEP_BEFORE_TRANSLATE:
case translate::TranslateStep::TRANSLATE_STEP_TRANSLATE_ERROR:
return TranslateInfobarViewStateBeforeTranslate;
case translate::TranslateStep::TRANSLATE_STEP_TRANSLATING:
return TranslateInfobarViewStateTranslating;
case translate::TranslateStep::TRANSLATE_STEP_AFTER_TRANSLATE:
return TranslateInfobarViewStateAfterTranslate;
case translate::TranslateStep::TRANSLATE_STEP_NEVER_TRANSLATE:
NOTREACHED() << "Translate infobar should never be in this state.";
return TranslateInfobarViewStateBeforeTranslate;
}
}
- (void)showTranslateOptionSelector {
[self.translateOptionSelectionHandler
showTranslateOptionSelectorWithInfoBarDelegate:self.infoBarDelegate
delegate:self];
[_infobarView updateUIForPopUpMenuDisplayed:YES];
}
- (void)showLanguageSelector {
int originalLanguageIndex = -1;
int targetLanguageIndex = -1;
for (size_t i = 0; i < self.infoBarDelegate->num_languages(); ++i) {
if (self.infoBarDelegate->language_code_at(i) ==
self.infoBarDelegate->original_language_code()) {
originalLanguageIndex = i;
}
if (self.infoBarDelegate->language_code_at(i) ==
self.infoBarDelegate->target_language_code()) {
targetLanguageIndex = i;
}
}
DCHECK_GE(originalLanguageIndex, 0);
DCHECK_GE(targetLanguageIndex, 0);
size_t selectedIndex;
size_t disabledIndex;
if (self.languageSelectionState == LanguageSelectionStateSource) {
selectedIndex = originalLanguageIndex;
disabledIndex = targetLanguageIndex;
} else {
selectedIndex = targetLanguageIndex;
disabledIndex = originalLanguageIndex;
}
LanguageSelectionContext* context =
[LanguageSelectionContext contextWithLanguageData:self.infoBarDelegate
initialIndex:selectedIndex
unavailableIndex:disabledIndex];
[self.languageSelectionHandler showLanguageSelectorWithContext:context
delegate:self];
[_infobarView updateUIForPopUpMenuDisplayed:YES];
}
- (void)recordInfobarEvent:(InfobarEvent)event {
UMA_HISTOGRAM_ENUMERATION(kEventHistogram, event);
}
- (void)recordLanguageDataHistogram:(const std::string&)histogram
languageCode:(const std::string&)langCode {
base::SparseHistogram::FactoryGet(
histogram, base::HistogramBase::kUmaTargetedHistogramFlag)
->Add(metrics::MetricsLog::Hash(langCode));
}
- (void)incrementAndRecordTranslationsCount {
UMA_HISTOGRAM_COUNTS_1M(kTranslationCountHistogram, ++self.translationsCount);
}
@end