blob: 861fcf52ac03149d463d3782be35b16bb9e5c05d [file] [log] [blame]
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/translate/ios/browser/ios_translate_driver.h"
#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/no_destructor.h"
#include "base/strings/sys_string_conversions.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "components/translate/core/browser/translate_client.h"
#include "components/translate/core/browser/translate_manager.h"
#include "components/translate/core/browser/translate_model_service.h"
#include "components/translate/core/common/language_detection_details.h"
#include "components/translate/core/common/translate_constants.h"
#include "components/translate/core/common/translate_metrics.h"
#include "components/translate/core/common/translate_util.h"
#include "components/translate/core/language_detection/language_detection_model.h"
#import "components/translate/ios/browser/js_translate_web_frame_manager_factory.h"
#include "components/translate/ios/browser/language_detection_model_service.h"
#import "components/translate/ios/browser/translate_controller.h"
#include "components/ukm/ios/ukm_url_recorder.h"
#import "ios/web/public/annotations/annotations_text_manager.h"
#include "ios/web/public/browser_state.h"
#include "ios/web/public/navigation/navigation_context.h"
#include "ios/web/public/navigation/navigation_item.h"
#include "ios/web/public/navigation/navigation_manager.h"
#include "ios/web/public/navigation/referrer.h"
#include "ios/web/public/thread/web_task_traits.h"
#include "ios/web/public/thread/web_thread.h"
#import "ios/web/public/web_state.h"
#include "ui/base/page_transition_types.h"
#include "ui/base/window_open_disposition.h"
#include "url/gurl.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace translate {
namespace {
// Language name passed to the Translate element for it to detect the language.
const char kAutoDetectionLanguage[] = "auto";
// Based on the cross platform value in
// //components/translate/content/renderer/per_frame_translate_agent.cc
// which checks every 400ms, up to 10 times.
// Double that time to take the script injection into account.
const base::TimeDelta kTimeoutDelay = base::Seconds(8);
} // namespace
IOSTranslateDriver::IOSTranslateDriver(
web::WebState* web_state,
LanguageDetectionModelService* language_detection_model_service)
: web_state_(web_state),
language_detection_model_service_(language_detection_model_service),
page_seq_no_(0),
pending_page_seq_no_(-1) {}
void IOSTranslateDriver::Initialize(
language::UrlLanguageHistogram* url_language_histogram,
TranslateManager* translate_manager) {
DCHECK(translate_manager);
DCHECK(web_state_);
translate_manager_ = translate_manager->GetWeakPtr();
web_state_->AddObserver(this);
LanguageDetectionModel* language_detection_model = nullptr;
if (language_detection_model_service_ && IsTFLiteLanguageDetectionEnabled()) {
language_detection_model =
language_detection_model_service_->GetLanguageDetectionModel();
}
language::IOSLanguageDetectionTabHelper::CreateForWebState(
web_state_, url_language_histogram, language_detection_model,
translate_manager_->translate_client()->GetPrefs());
language::IOSLanguageDetectionTabHelper::FromWebState(web_state_)
->AddObserver(this);
TranslateController::CreateForWebState(
web_state_, JSTranslateWebFrameManagerFactory::GetInstance());
TranslateController::FromWebState(web_state_)->set_observer(this);
}
IOSTranslateDriver::~IOSTranslateDriver() {
if (web_state_) {
StopObservingIOSLanguageDetectionTabHelper();
StopObservingWebState();
}
}
void IOSTranslateDriver::OnLanguageDetermined(
const LanguageDetectionDetails& details) {
if (!translate_manager_)
return;
translate_manager_->GetLanguageState()->LanguageDetermined(
details.adopted_language, true);
// Don't offer translation on pages with notranslate meta tag.
if (web_state_ && !details.has_notranslate)
translate_manager_->InitiateTranslation(details.adopted_language);
for (auto& observer : language_detection_observers())
observer.OnLanguageDetermined(details);
}
void IOSTranslateDriver::IOSLanguageDetectionTabHelperWasDestroyed(
language::IOSLanguageDetectionTabHelper* tab_helper) {
// No-op. We stop observing the IOSLanguageDetectionTabHelper in
// IOSTranslateDriver::WebStateDestroyed.
}
// web::WebStateObserver methods
void IOSTranslateDriver::DidStartNavigation(
web::WebState* web_state,
web::NavigationContext* navigation_context) {
DCHECK_EQ(web_state_, web_state);
if (!navigation_context->IsSameDocument()) {
pending_page_seq_no_ = -1;
timeout_timer_.Stop();
}
}
void IOSTranslateDriver::DidFinishNavigation(
web::WebState* web_state,
web::NavigationContext* navigation_context) {
DCHECK_EQ(web_state_, web_state);
// Interrupt pending translations and reset various data when a navigation
// happens. Desktop does it by tracking changes in the page ID, and
// through WebContentObserver, but these concepts do not exist on iOS.
if (!navigation_context->IsSameDocument()) {
++page_seq_no_;
translate_manager_->set_current_seq_no(page_seq_no_);
}
// TODO(crbug.com/925320): support navigation types, like content/ does.
const bool reload = ui::PageTransitionCoreTypeIs(
navigation_context->GetPageTransition(), ui::PAGE_TRANSITION_RELOAD);
translate_manager_->GetLanguageState()->DidNavigate(
navigation_context->IsSameDocument(), true, reload, std::string(), false);
}
void IOSTranslateDriver::WebStateDestroyed(web::WebState* web_state) {
DCHECK_EQ(web_state_, web_state);
timeout_timer_.Stop();
StopObservingIOSLanguageDetectionTabHelper();
StopObservingWebState();
}
// TranslateDriver methods
bool IOSTranslateDriver::IsLinkNavigation() {
DCHECK(web_state_->IsRealized());
web::NavigationItem* visible_item =
web_state_->GetNavigationManager()->GetVisibleItem();
return visible_item &&
ui::PageTransitionCoreTypeIs(visible_item->GetTransitionType(),
ui::PAGE_TRANSITION_LINK);
}
void IOSTranslateDriver::OnTranslateEnabledChanged() {
}
void IOSTranslateDriver::OnIsPageTranslatedChanged() {
}
void IOSTranslateDriver::PrepareToTranslatePage(
int page_seq_no,
const std::string& original_source_lang,
const std::string& target_lang,
bool triggered_from_menu) {
if (!IsPageValid(page_seq_no))
return; // The user navigated away.
pending_page_seq_no_ = page_seq_no;
source_language_ = original_source_lang;
target_language_ = target_lang;
timeout_timer_.Start(FROM_HERE, kTimeoutDelay,
BindOnce(&IOSTranslateDriver::OnTranslationTimeout,
weak_ptr_factory_.GetWeakPtr(), page_seq_no));
// Remove annotations before replacing translated data.
auto* manager = web::AnnotationsTextManager::FromWebState(web_state_);
if (manager) {
manager->RemoveDecorations();
}
}
void IOSTranslateDriver::TranslatePage(int page_seq_no,
const std::string& translate_script,
const std::string& source_lang,
const std::string& target_lang) {
if (!IsPageValid(pending_page_seq_no_))
return; // The user navigated away.
source_language_ = source_lang;
target_language_ = target_lang;
TranslateController::FromWebState(web_state_)
->InjectTranslateScript(translate_script);
}
void IOSTranslateDriver::OnTranslationTimeout(int page_seq_no) {
if (!IsPageValid(pending_page_seq_no_) || !IsPageValid(page_seq_no))
return; // The user navigated away or timeout is obsolete.
translate_manager_->PageTranslated(source_language_, target_language_,
TranslateErrors::TRANSLATION_TIMEOUT);
pending_page_seq_no_ = -1;
RevertTranslation(page_seq_no);
}
void IOSTranslateDriver::RevertTranslation(int page_seq_no) {
if (page_seq_no != page_seq_no_)
return; // The user navigated away.
timeout_timer_.Stop();
TranslateController::FromWebState(web_state_)->RevertTranslation();
}
bool IOSTranslateDriver::IsIncognito() {
return web_state_->GetBrowserState()->IsOffTheRecord();
}
const std::string& IOSTranslateDriver::GetContentsMimeType() {
return web_state_->GetContentsMimeType();
}
const GURL& IOSTranslateDriver::GetLastCommittedURL() {
return web_state_->GetLastCommittedURL();
}
const GURL& IOSTranslateDriver::GetVisibleURL() {
return web_state_->GetVisibleURL();
}
ukm::SourceId IOSTranslateDriver::GetUkmSourceId() {
return ukm::GetSourceIdForWebStateDocument(web_state_);
}
bool IOSTranslateDriver::HasCurrentPage() {
DCHECK(web_state_->IsRealized());
return (web_state_->GetNavigationManager()->GetVisibleItem() != nullptr);
}
void IOSTranslateDriver::OpenUrlInNewTab(const GURL& url) {
web::WebState::OpenURLParams params(url, web::Referrer(),
WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui::PAGE_TRANSITION_LINK, false);
web_state_->OpenURL(params);
}
void IOSTranslateDriver::TranslationDidSucceed(
const std::string& source_lang,
const std::string& target_lang,
int page_seq_no,
const std::string& original_page_language,
double translation_time) {
if (!IsPageValid(page_seq_no))
return;
std::string actual_source_lang;
TranslateErrors translate_errors = TranslateErrors::NONE;
// Translation was successfull; if it was auto, retrieve the source
// language the Translate Element detected.
if (source_lang == kAutoDetectionLanguage) {
actual_source_lang = original_page_language;
if (actual_source_lang.empty()) {
translate_errors = TranslateErrors::UNKNOWN_LANGUAGE;
} else if (actual_source_lang == target_lang) {
translate_errors = TranslateErrors::IDENTICAL_LANGUAGES;
}
} else {
actual_source_lang = source_lang;
}
if (translate_errors == TranslateErrors::NONE)
ReportTimeToTranslate(translation_time);
// Notify the manage of completion.
translate_manager_->PageTranslated(actual_source_lang, target_lang,
translate_errors);
}
bool IOSTranslateDriver::IsPageValid(int page_seq_no) const {
bool user_navigated_away = page_seq_no != page_seq_no_;
return !user_navigated_away && web_state_;
}
// TranslateController::Observer implementation.
void IOSTranslateDriver::OnTranslateScriptReady(TranslateErrors error_type,
double load_time,
double ready_time) {
if (!IsPageValid(pending_page_seq_no_))
return;
if (error_type != TranslateErrors::NONE) {
translate_manager_->PageTranslated(source_language_, target_language_,
error_type);
return;
}
ReportTimeToLoad(load_time);
ReportTimeToBeReady(ready_time);
std::string source = (source_language_ != kUnknownLanguageCode)
? source_language_
: kAutoDetectionLanguage;
TranslateController::FromWebState(web_state_)
->StartTranslation(source, target_language_);
}
void IOSTranslateDriver::OnTranslateComplete(TranslateErrors error_type,
const std::string& source_language,
double translation_time) {
if (!IsPageValid(pending_page_seq_no_))
return;
if (error_type != TranslateErrors::NONE) {
translate_manager_->PageTranslated(source_language_, target_language_,
error_type);
return;
}
TranslationDidSucceed(source_language_, target_language_,
pending_page_seq_no_, source_language,
translation_time);
pending_page_seq_no_ = -1;
timeout_timer_.Stop();
}
void IOSTranslateDriver::StopObservingWebState() {
web_state_->RemoveObserver(this);
web_state_ = nullptr;
timeout_timer_.Stop();
}
void IOSTranslateDriver::StopObservingIOSLanguageDetectionTabHelper() {
DCHECK(web_state_);
language::IOSLanguageDetectionTabHelper* language_detection_tab_helper =
language::IOSLanguageDetectionTabHelper::FromWebState(web_state_);
language_detection_tab_helper->RemoveObserver(this);
}
} // namespace translate