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