1// Copyright 2019 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#import "ios/chrome/browser/translate/translate_infobar_controller.h"
6
7#import <UIKit/UIKit.h>
8
9#include <stddef.h>
10#include <memory>
11
12#include "base/check_op.h"
13#include "base/mac/foundation_util.h"
14#include "base/metrics/histogram_macros.h"
15#include "base/metrics/sparse_histogram.h"
16#include "base/notreached.h"
17#include "base/optional.h"
18#include "base/strings/sys_string_conversions.h"
19#include "components/metrics/metrics_log.h"
20#include "components/strings/grit/components_strings.h"
21#include "components/translate/core/browser/translate_infobar_delegate.h"
22#include "components/translate/core/common/translate_constants.h"
23#import "ios/chrome/browser/infobars/infobar_controller+protected.h"
24#include "ios/chrome/browser/infobars/infobar_controller_delegate.h"
25#include "ios/chrome/browser/translate/language_selection_context.h"
26#include "ios/chrome/browser/translate/language_selection_delegate.h"
27#include "ios/chrome/browser/translate/language_selection_handler.h"
28#import "ios/chrome/browser/translate/translate_constants.h"
29#import "ios/chrome/browser/translate/translate_infobar_delegate_observer_bridge.h"
30#import "ios/chrome/browser/translate/translate_infobar_metrics_recorder.h"
31#include "ios/chrome/browser/translate/translate_option_selection_delegate.h"
32#include "ios/chrome/browser/translate/translate_option_selection_handler.h"
33#import "ios/chrome/browser/ui/translate/translate_infobar_view.h"
34#import "ios/chrome/browser/ui/translate/translate_infobar_view_delegate.h"
35#import "ios/chrome/browser/ui/translate/translate_notification_delegate.h"
36#import "ios/chrome/browser/ui/translate/translate_notification_handler.h"
37#include "ui/base/l10n/l10n_util.h"
38#include "ui/gfx/image/image.h"
39
40#if !defined(__has_feature) || !__has_feature(objc_arc)
41#error "This file requires ARC support."
42#endif
43
44namespace {
45
46// Whether langugage selection popup menu is offered; and whether it is to
47// select the source or the target language.
48typedef NS_ENUM(NSInteger, LanguageSelectionState) {
49  LanguageSelectionStateNone,
50  LanguageSelectionStateSource,
51  LanguageSelectionStateTarget,
52};
53
54}  // namespace
55
56@interface TranslateInfoBarController () <LanguageSelectionDelegate,
57                                          TranslateInfobarDelegateObserving,
58                                          TranslateInfobarViewDelegate,
59                                          TranslateNotificationDelegate,
60                                          TranslateOptionSelectionDelegate> {
61  std::unique_ptr<TranslateInfobarDelegateObserverBridge>
62      _translateInfobarDelegateObserver;
63}
64
65// Overrides superclass property.
66@property(nonatomic, readonly)
67    translate::TranslateInfoBarDelegate* infoBarDelegate;
68
69@property(nonatomic, weak) TranslateInfobarView* infobarView;
70
71// Indicates whether langugage selection popup menu is offered; and whether it
72// is to select the source or the target language.
73@property(nonatomic, assign) LanguageSelectionState languageSelectionState;
74
75// Tracks user actions.
76@property(nonatomic, assign) UserAction userAction;
77
78// The NSDate during which the infobar was displayed.
79@property(nonatomic, strong) NSDate* infobarDisplayTime;
80
81// The NSDate of when a Translate or a revert was last executed.
82@property(nonatomic, strong) NSDate* lastTranslateTime;
83
84// Tracks the total number of translations in a page, incl. reverts to original.
85@property(nonatomic, assign) NSUInteger translationsCount;
86
87@end
88
89@implementation TranslateInfoBarController
90
91@dynamic infoBarDelegate;
92
93#pragma mark - InfoBarControllerProtocol
94
95- (instancetype)initWithInfoBarDelegate:
96    (translate::TranslateInfoBarDelegate*)infoBarDelegate {
97  self = [super initWithInfoBarDelegate:infoBarDelegate];
98  if (self) {
99    _translateInfobarDelegateObserver =
100        std::make_unique<TranslateInfobarDelegateObserverBridge>(
101            infoBarDelegate, self);
102    _userAction = UserActionNone;
103
104    [self recordInfobarEvent:translate::InfobarEvent::INFOBAR_IMPRESSION];
105  }
106  return self;
107}
108
109- (void)dealloc {
110  if (self.userAction == UserActionNone) {
111    NSTimeInterval displayDuration =
112        [[NSDate date] timeIntervalSinceDate:self.infobarDisplayTime];
113    [TranslateInfobarMetricsRecorder
114        recordUnusedLegacyInfobarScreenDuration:displayDuration];
115    [TranslateInfobarMetricsRecorder recordUnusedInfobar];
116  }
117}
118
119- (UIView*)infobarView {
120  TranslateInfobarView* infobarView =
121      [[TranslateInfobarView alloc] initWithFrame:CGRectZero];
122  // |_infobarView| is referenced inside |-updateUIForTranslateStep:|.
123  _infobarView = infobarView;
124  infobarView.sourceLanguage = self.sourceLanguage;
125  infobarView.targetLanguage = self.targetLanguage;
126  infobarView.delegate = self;
127  [self updateUIForTranslateStep:self.infoBarDelegate->translate_step()];
128  self.infobarDisplayTime = [NSDate date];
129  return infobarView;
130}
131
132#pragma mark - TranslateInfobarDelegateObserving
133
134- (void)translateInfoBarDelegate:(translate::TranslateInfoBarDelegate*)delegate
135          didChangeTranslateStep:(translate::TranslateStep)step
136                   withErrorType:(translate::TranslateErrors::Type)errorType {
137  [self updateUIForTranslateStep:step];
138
139  if (step == translate::TranslateStep::TRANSLATE_STEP_TRANSLATE_ERROR ||
140      step == translate::TranslateStep::TRANSLATE_STEP_AFTER_TRANSLATE) {
141    [self incrementAndRecordTranslationsCount];
142  }
143}
144
145- (BOOL)translateInfoBarDelegateDidDismissWithoutInteraction:
146    (translate::TranslateInfoBarDelegate*)delegate {
147  return self.userAction == UserActionNone;
148}
149
150#pragma mark - TranslateInfobarViewDelegate
151
152- (void)translateInfobarViewDidTapSourceLangugage:
153    (TranslateInfobarView*)sender {
154  // If already showing original language, no need to revert translate.
155  if (sender.state == TranslateInfobarViewStateBeforeTranslate)
156    return;
157  if ([self shouldIgnoreUserInteraction])
158    return;
159
160  self.userAction |= UserActionRevert;
161  if (self.userAction & UserActionTranslate) {
162    // Log the time between the last translate and this revert.
163    NSTimeInterval duration =
164        [[NSDate date] timeIntervalSinceDate:self.lastTranslateTime];
165    [TranslateInfobarMetricsRecorder recordLegacyInfobarToggleDelay:duration];
166  }
167  self.lastTranslateTime = [NSDate date];
168
169  [self recordInfobarEvent:translate::InfobarEvent::INFOBAR_REVERT];
170  [self incrementAndRecordTranslationsCount];
171
172  self.infoBarDelegate->RevertWithoutClosingInfobar();
173  _infobarView.state = TranslateInfobarViewStateBeforeTranslate;
174}
175
176- (void)translateInfobarViewDidTapTargetLangugage:
177    (TranslateInfobarView*)sender {
178  // If already showing target language, no need to translate.
179  if (sender.state == TranslateInfobarViewStateAfterTranslate)
180    return;
181  if ([self shouldIgnoreUserInteraction])
182    return;
183
184  self.userAction |= UserActionTranslate;
185  if (self.userAction & UserActionRevert) {
186    // Log the time between the last revert and this translate.
187    NSTimeInterval duration =
188        [[NSDate date] timeIntervalSinceDate:self.lastTranslateTime];
189    [TranslateInfobarMetricsRecorder recordLegacyInfobarToggleDelay:duration];
190  }
191  self.lastTranslateTime = [NSDate date];
192
193  [self
194      recordInfobarEvent:translate::InfobarEvent::INFOBAR_TARGET_TAB_TRANSLATE];
195  [self
196      recordLanguageDataHistogram:kLanguageHistogramTranslate
197                     languageCode:self.infoBarDelegate->target_language_code()];
198
199  if (self.infoBarDelegate->ShouldAutoAlwaysTranslate()) {
200    [self recordInfobarEvent:translate::InfobarEvent::
201                                 INFOBAR_SNACKBAR_AUTO_ALWAYS_IMPRESSION];
202
203    // Page will be translated once the snackbar finishes showing.
204    [self.translateNotificationHandler
205        showTranslateNotificationWithDelegate:self
206                             notificationType:
207                                 TranslateNotificationTypeAutoAlwaysTranslate];
208  } else {
209    self.infoBarDelegate->Translate();
210  }
211}
212
213- (void)translateInfobarViewDidTapOptions:(TranslateInfobarView*)sender {
214  if ([self shouldIgnoreUserInteraction])
215    return;
216
217  self.userAction |= UserActionExpandMenu;
218
219  [self recordInfobarEvent:translate::InfobarEvent::INFOBAR_OPTIONS];
220
221  [self showTranslateOptionSelector];
222}
223
224- (void)translateInfobarViewDidTapDismiss:(TranslateInfobarView*)sender {
225  if ([self shouldIgnoreUserInteraction])
226    return;
227
228  if (self.userAction == UserActionNone) {
229    [self recordInfobarEvent:translate::InfobarEvent::INFOBAR_DECLINE];
230  }
231
232  if (self.infoBarDelegate->ShouldAutoNeverTranslate()) {
233    [self recordInfobarEvent:translate::InfobarEvent::
234                                 INFOBAR_SNACKBAR_AUTO_NEVER_IMPRESSION];
235
236    // Infobar will dismiss once the snackbar finishes showing.
237    [self.translateNotificationHandler
238        showTranslateNotificationWithDelegate:self
239                             notificationType:
240                                 TranslateNotificationTypeAutoNeverTranslate];
241  } else {
242    self.infoBarDelegate->InfoBarDismissed();
243    self.delegate->RemoveInfoBar();
244  }
245}
246
247#pragma mark - LanguageSelectionDelegate
248
249- (void)languageSelectorSelectedLanguage:(std::string)languageCode {
250  if (self.languageSelectionState == LanguageSelectionStateSource) {
251    [self recordLanguageDataHistogram:kLanguageHistogramPageNotInLanguage
252                         languageCode:languageCode];
253
254    self.infoBarDelegate->UpdateOriginalLanguage(languageCode);
255    _infobarView.sourceLanguage = self.sourceLanguage;
256  } else {
257    [self recordInfobarEvent:translate::InfobarEvent::
258                                 INFOBAR_MORE_LANGUAGES_TRANSLATE];
259    [self recordLanguageDataHistogram:kLanguageHistogramMoreLanguages
260                         languageCode:languageCode];
261
262    self.infoBarDelegate->UpdateTargetLanguage(languageCode);
263    _infobarView.targetLanguage = self.targetLanguage;
264  }
265  self.languageSelectionState = LanguageSelectionStateNone;
266
267  self.infoBarDelegate->Translate();
268
269  [_infobarView updateUIForPopUpMenuDisplayed:NO];
270}
271
272- (void)languageSelectorClosedWithoutSelection {
273  self.languageSelectionState = LanguageSelectionStateNone;
274
275  [_infobarView updateUIForPopUpMenuDisplayed:NO];
276}
277
278#pragma mark - TranslateOptionSelectionDelegate
279
280- (void)popupMenuTableViewControllerDidSelectTargetLanguageSelector:
281    (PopupMenuTableViewController*)sender {
282  if ([self shouldIgnoreUserInteraction])
283    return;
284
285  self.userAction |= UserActionExpandMenu;
286
287  [self recordInfobarEvent:translate::InfobarEvent::INFOBAR_MORE_LANGUAGES];
288
289  [_infobarView updateUIForPopUpMenuDisplayed:NO];
290
291  self.languageSelectionState = LanguageSelectionStateTarget;
292  [self showLanguageSelector];
293}
294
295- (void)popupMenuTableViewControllerDidSelectAlwaysTranslateSourceLanguage:
296    (PopupMenuTableViewController*)sender {
297  if ([self shouldIgnoreUserInteraction])
298    return;
299
300  self.userAction |= UserActionAlwaysTranslate;
301
302  [_infobarView updateUIForPopUpMenuDisplayed:NO];
303
304  if (self.infoBarDelegate->ShouldAlwaysTranslate()) {
305    [self recordInfobarEvent:translate::InfobarEvent::
306                                 INFOBAR_ALWAYS_TRANSLATE_UNDO];
307
308    self.infoBarDelegate->ToggleAlwaysTranslate();
309  } else {
310    [self recordInfobarEvent:translate::InfobarEvent::INFOBAR_ALWAYS_TRANSLATE];
311    [self recordInfobarEvent:translate::InfobarEvent::
312                                 INFOBAR_SNACKBAR_ALWAYS_TRANSLATE_IMPRESSION];
313    [self recordLanguageDataHistogram:kLanguageHistogramAlwaysTranslate
314                         languageCode:self.infoBarDelegate
315                                          ->original_language_code()];
316
317    // Page will be translated once the snackbar finishes showing.
318    [self.translateNotificationHandler
319        showTranslateNotificationWithDelegate:self
320                             notificationType:
321                                 TranslateNotificationTypeAlwaysTranslate];
322  }
323}
324
325- (void)popupMenuTableViewControllerDidSelectNeverTranslateSourceLanguage:
326    (PopupMenuTableViewController*)sender {
327  if ([self shouldIgnoreUserInteraction])
328    return;
329
330  self.userAction |= UserActionNeverTranslateLanguage;
331
332  [_infobarView updateUIForPopUpMenuDisplayed:NO];
333
334  if (self.infoBarDelegate->IsTranslatableLanguageByPrefs()) {
335    [self recordInfobarEvent:translate::InfobarEvent::INFOBAR_NEVER_TRANSLATE];
336    [self recordInfobarEvent:translate::InfobarEvent::
337                                 INFOBAR_SNACKBAR_NEVER_TRANSLATE_IMPRESSION];
338    [self recordLanguageDataHistogram:kLanguageHistogramNeverTranslate
339                         languageCode:self.infoBarDelegate
340                                          ->original_language_code()];
341
342    // Infobar will dismiss once the snackbar finishes showing.
343    [self.translateNotificationHandler
344        showTranslateNotificationWithDelegate:self
345                             notificationType:
346                                 TranslateNotificationTypeNeverTranslate];
347  }
348}
349
350- (void)popupMenuTableViewControllerDidSelectNeverTranslateSite:
351    (PopupMenuTableViewController*)sender {
352  if ([self shouldIgnoreUserInteraction])
353    return;
354
355  self.userAction |= UserActionNeverTranslateSite;
356
357  [_infobarView updateUIForPopUpMenuDisplayed:NO];
358
359  if (!self.infoBarDelegate->IsSiteBlacklisted()) {
360    [self recordInfobarEvent:translate::InfobarEvent::
361                                 INFOBAR_NEVER_TRANSLATE_SITE];
362    [self recordInfobarEvent:
363              translate::InfobarEvent::
364                  INFOBAR_SNACKBAR_NEVER_TRANSLATE_SITE_IMPRESSION];
365
366    // Infobar will dismiss once the snackbar finishes showing.
367    [self.translateNotificationHandler
368        showTranslateNotificationWithDelegate:self
369                             notificationType:
370                                 TranslateNotificationTypeNeverTranslateSite];
371  }
372}
373
374- (void)popupMenuTableViewControllerDidSelectSourceLanguageSelector:
375    (PopupMenuTableViewController*)sender {
376  if ([self shouldIgnoreUserInteraction])
377    return;
378
379  self.userAction |= UserActionExpandMenu;
380
381  [self recordInfobarEvent:translate::InfobarEvent::INFOBAR_PAGE_NOT_IN];
382
383  [_infobarView updateUIForPopUpMenuDisplayed:NO];
384
385  self.languageSelectionState = LanguageSelectionStateSource;
386  [self showLanguageSelector];
387}
388
389- (void)popupMenuPresenterDidCloseWithoutSelection:(PopupMenuPresenter*)sender {
390  [_infobarView updateUIForPopUpMenuDisplayed:NO];
391}
392
393#pragma mark - TranslateNotificationDelegate
394
395- (void)translateNotificationHandlerDidDismiss:
396            (id<TranslateNotificationHandler>)sender
397                              notificationType:(TranslateNotificationType)type {
398  switch (type) {
399    case TranslateNotificationTypeAlwaysTranslate:
400    case TranslateNotificationTypeAutoAlwaysTranslate:
401      self.infoBarDelegate->ToggleAlwaysTranslate();
402      self.infoBarDelegate->Translate();
403      break;
404    case TranslateNotificationTypeAutoNeverTranslate:
405      self.infoBarDelegate->InfoBarDismissed();
406      FALLTHROUGH;
407    case TranslateNotificationTypeNeverTranslate:
408      self.infoBarDelegate->ToggleTranslatableLanguageByPrefs();
409      self.delegate->RemoveInfoBar();
410      break;
411    case TranslateNotificationTypeNeverTranslateSite:
412      self.infoBarDelegate->ToggleSiteBlacklist();
413      self.delegate->RemoveInfoBar();
414      break;
415    case TranslateNotificationTypeError:
416      // No-op.
417      break;
418  }
419}
420
421- (void)translateNotificationHandlerDidUndo:
422            (id<TranslateNotificationHandler>)sender
423                           notificationType:(TranslateNotificationType)type {
424  switch (type) {
425    case TranslateNotificationTypeAlwaysTranslate:
426      [self recordInfobarEvent:translate::InfobarEvent::
427                                   INFOBAR_SNACKBAR_CANCEL_ALWAYS];
428      break;
429    case TranslateNotificationTypeAutoAlwaysTranslate:
430      [self recordInfobarEvent:translate::InfobarEvent::
431                                   INFOBAR_SNACKBAR_CANCEL_AUTO_ALWAYS];
432      break;
433    case TranslateNotificationTypeNeverTranslate:
434      [self recordInfobarEvent:translate::InfobarEvent::
435                                   INFOBAR_SNACKBAR_CANCEL_NEVER];
436      break;
437    case TranslateNotificationTypeAutoNeverTranslate:
438      [self recordInfobarEvent:translate::InfobarEvent::
439                                   INFOBAR_SNACKBAR_CANCEL_AUTO_NEVER];
440      // Remove the infobar even if the user tapped "Undo" since user explicitly
441      // dismissed the infobar.
442      self.infoBarDelegate->InfoBarDismissed();
443      self.delegate->RemoveInfoBar();
444      break;
445    case TranslateNotificationTypeNeverTranslateSite:
446      [self recordInfobarEvent:translate::InfobarEvent::
447                                   INFOBAR_SNACKBAR_CANCEL_NEVER_SITE];
448      break;
449    case TranslateNotificationTypeError:
450      // No-op.
451      break;
452  }
453}
454
455- (NSString*)sourceLanguage {
456  return base::SysUTF16ToNSString(
457      self.infoBarDelegate->original_language_name());
458}
459
460- (NSString*)targetLanguage {
461  return base::SysUTF16ToNSString(self.infoBarDelegate->target_language_name());
462}
463
464#pragma mark - Private
465
466// Updates the infobar view state for the given translate::TranslateStep. Shows
467// an error for translate::TranslateStep::TRANSLATE_STEP_TRANSLATE_ERROR.
468- (void)updateUIForTranslateStep:(translate::TranslateStep)step {
469  switch (step) {
470    case translate::TranslateStep::TRANSLATE_STEP_TRANSLATE_ERROR:
471      [self.translateNotificationHandler
472          showTranslateNotificationWithDelegate:self
473                               notificationType:TranslateNotificationTypeError];
474      FALLTHROUGH;
475    case translate::TranslateStep::TRANSLATE_STEP_BEFORE_TRANSLATE:
476      _infobarView.state = TranslateInfobarViewStateBeforeTranslate;
477      break;
478    case translate::TranslateStep::TRANSLATE_STEP_TRANSLATING:
479      _infobarView.state = TranslateInfobarViewStateTranslating;
480      break;
481    case translate::TranslateStep::TRANSLATE_STEP_AFTER_TRANSLATE:
482      _infobarView.state = TranslateInfobarViewStateAfterTranslate;
483      break;
484    case translate::TranslateStep::TRANSLATE_STEP_NEVER_TRANSLATE:
485      NOTREACHED() << "Translate infobar should never be in this state.";
486      break;
487  }
488}
489
490- (void)showTranslateOptionSelector {
491  [self.translateOptionSelectionHandler
492      showTranslateOptionSelectorWithInfoBarDelegate:self.infoBarDelegate
493                                            delegate:self];
494  [_infobarView updateUIForPopUpMenuDisplayed:YES];
495}
496
497- (void)showLanguageSelector {
498  int originalLanguageIndex = -1;
499  int targetLanguageIndex = -1;
500  for (size_t i = 0; i < self.infoBarDelegate->num_languages(); ++i) {
501    if (self.infoBarDelegate->language_code_at(i) ==
502        self.infoBarDelegate->original_language_code()) {
503      originalLanguageIndex = i;
504    }
505    if (self.infoBarDelegate->language_code_at(i) ==
506        self.infoBarDelegate->target_language_code()) {
507      targetLanguageIndex = i;
508    }
509  }
510  DCHECK_GE(originalLanguageIndex, 0);
511  DCHECK_GE(targetLanguageIndex, 0);
512
513  size_t selectedIndex;
514  size_t disabledIndex;
515  if (self.languageSelectionState == LanguageSelectionStateSource) {
516    selectedIndex = originalLanguageIndex;
517    disabledIndex = targetLanguageIndex;
518  } else {
519    selectedIndex = targetLanguageIndex;
520    disabledIndex = originalLanguageIndex;
521  }
522  LanguageSelectionContext* context =
523      [LanguageSelectionContext contextWithLanguageData:self.infoBarDelegate
524                                           initialIndex:selectedIndex
525                                       unavailableIndex:disabledIndex];
526  [self.languageSelectionHandler showLanguageSelectorWithContext:context
527                                                        delegate:self];
528  [_infobarView updateUIForPopUpMenuDisplayed:YES];
529}
530
531- (void)recordInfobarEvent:(translate::InfobarEvent)event {
532  UMA_HISTOGRAM_ENUMERATION(kEventHistogram, event);
533}
534
535- (void)recordLanguageDataHistogram:(const std::string&)histogram
536                       languageCode:(const std::string&)langCode {
537  base::SparseHistogram::FactoryGet(
538      histogram, base::HistogramBase::kUmaTargetedHistogramFlag)
539      ->Add(metrics::MetricsLog::Hash(langCode));
540}
541
542- (void)incrementAndRecordTranslationsCount {
543  UMA_HISTOGRAM_COUNTS_1M(kTranslationCountHistogram, ++self.translationsCount);
544}
545
546@end
547