diff options
author | droger <droger@chromium.org> | 2014-12-16 13:58:13 -0800 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2014-12-16 21:58:38 +0000 |
commit | bfba8a470717488726b909c8208f15c6a4512673 (patch) | |
tree | 56df55800551a343f58e3f46207971d58c1ae645 /components/translate | |
parent | 634a76e456748851c4d51a89a6b466b3f2d972cc (diff) | |
download | chromium_src-bfba8a470717488726b909c8208f15c6a4512673.zip chromium_src-bfba8a470717488726b909c8208f15c6a4512673.tar.gz chromium_src-bfba8a470717488726b909c8208f15c6a4512673.tar.bz2 |
Upstream components/translate/ios
Review URL: https://codereview.chromium.org/809693003
Cr-Commit-Position: refs/heads/master@{#308676}
Diffstat (limited to 'components/translate')
16 files changed, 1666 insertions, 0 deletions
diff --git a/components/translate/ios/DEPS b/components/translate/ios/DEPS new file mode 100644 index 0000000..4dd6307 --- /dev/null +++ b/components/translate/ios/DEPS @@ -0,0 +1,4 @@ +include_rules = [ + "+ios/web/public", + "+third_party/ocmock", +] diff --git a/components/translate/ios/browser/ios_translate_driver.h b/components/translate/ios/browser/ios_translate_driver.h new file mode 100644 index 0000000..ee47d44 --- /dev/null +++ b/components/translate/ios/browser/ios_translate_driver.h @@ -0,0 +1,128 @@ +// Copyright 2014 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. + +#ifndef COMPONENTS_TRANSLATE_IOS_BROWSER_IOS_TRANSLATE_DRIVER_H_ +#define COMPONENTS_TRANSLATE_IOS_BROWSER_IOS_TRANSLATE_DRIVER_H_ + +#include <string> + +#include "base/basictypes.h" +#include "base/memory/weak_ptr.h" +#include "components/translate/core/browser/translate_driver.h" +#include "components/translate/ios/browser/language_detection_controller.h" +#include "components/translate/ios/browser/translate_controller.h" +#include "ios/web/public/web_state/web_state_observer.h" + +@class CRWJSInjectionReceiver; + +namespace web { +class NavigationManager; +class WebState; +} + +namespace translate { + +class TranslateManager; + +// Content implementation of TranslateDriver. +class IOSTranslateDriver : public TranslateDriver, + public TranslateController::Observer, + public web::WebStateObserver { + public: + IOSTranslateDriver(web::WebState* web_state, + web::NavigationManager* navigation_manager, + TranslateManager* translate_manager); + ~IOSTranslateDriver() override; + + LanguageDetectionController* language_detection_controller() { + return language_detection_controller_.get(); + } + + TranslateController* translate_controller() { + return translate_controller_.get(); + } + + // web::WebStateObserver methods. + void NavigationItemCommitted( + const web::LoadCommittedDetails& load_details) override; + + // TranslateDriver methods. + void OnIsPageTranslatedChanged() override; + void OnTranslateEnabledChanged() override; + bool IsLinkNavigation() override; + void TranslatePage(int page_seq_no, + const std::string& translate_script, + const std::string& source_lang, + const std::string& target_lang) override; + void RevertTranslation(int page_seq_no) override; + bool IsOffTheRecord() override; + const std::string& GetContentsMimeType() override; + const GURL& GetLastCommittedURL() override; + const GURL& GetActiveURL() override; + const GURL& GetVisibleURL() override; + bool HasCurrentPage() override; + void OpenUrlInNewTab(const GURL& url) override; + + private: + // Called when the translation was successfull. + void TranslationDidSucceed(const std::string& source_lang, + const std::string& target_lang, + int page_seq_no, + const std::string& original_page_language, + double translation_time); + // Checks if the current running page translation is finished or errored and + // notifies the browser accordingly. If the translation has not terminated, + // posts a task to check again later. + // Similar to TranslateHelper::CheckTranslateStatus on desktop. + void CheckTranslateStatus(const std::string& source_language, + const std::string& target_language, + int page_seq_no); + + // Returns true if the user has not navigated away and the the page is not + // being destroyed. + bool IsPageValid(int page_seq_no) const; + + // Callback for LanguageDetectionController. + void OnLanguageDetermined( + const LanguageDetectionController::DetectionDetails& details); + + // TranslateController::Observer methods. + void OnTranslateScriptReady(bool success, + double load_time, + double ready_time) override; + void OnTranslateComplete(bool success, + const std::string& original_language, + double translation_time) override; + + // The navigation manager of the tab we are associated with. + web::NavigationManager* navigation_manager_; + + base::WeakPtr<TranslateManager> translate_manager_; + scoped_ptr<TranslateController> translate_controller_; + scoped_ptr<LanguageDetectionController> language_detection_controller_; + scoped_ptr<LanguageDetectionController::CallbackList::Subscription> + language_detection_callback_subscription_; + + // An ever-increasing sequence number of the current page, used to match up + // translation requests with responses. + // This matches the similar field in TranslateHelper in the renderer on other + // platforms. + int page_seq_no_; + + // When a translation is in progress, its page sequence number is stored in + // |pending_page_seq_no_|. + int pending_page_seq_no_; + + // Parameters of the current translation. + std::string source_language_; + std::string target_language_; + + base::WeakPtrFactory<IOSTranslateDriver> weak_method_factory_; + + DISALLOW_COPY_AND_ASSIGN(IOSTranslateDriver); +}; + +} // namespace translate + +#endif // COMPONENTS_TRANSLATE_IOS_BROWSER_IOS_TRANSLATE_DRIVER_H_ diff --git a/components/translate/ios/browser/ios_translate_driver.mm b/components/translate/ios/browser/ios_translate_driver.mm new file mode 100644 index 0000000..d6d2117 --- /dev/null +++ b/components/translate/ios/browser/ios_translate_driver.mm @@ -0,0 +1,271 @@ +// Copyright 2014 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. + +#include "components/translate/ios/browser/ios_translate_driver.h" + +#include "base/bind.h" +#include "base/location.h" +#include "base/logging.h" +#include "base/message_loop/message_loop.h" +#include "base/strings/sys_string_conversions.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/common/translate_constants.h" +#include "components/translate/core/common/translate_errors.h" +#include "components/translate/core/common/translate_metrics.h" +#import "components/translate/ios/browser/js_language_detection_manager.h" +#import "components/translate/ios/browser/js_translate_manager.h" +#import "components/translate/ios/browser/language_detection_controller.h" +#import "components/translate/ios/browser/translate_controller.h" +#include "ios/web/public/browser_state.h" +#include "ios/web/public/load_committed_details.h" +#include "ios/web/public/navigation_item.h" +#include "ios/web/public/navigation_manager.h" +#include "ios/web/public/referrer.h" +#include "ios/web/public/web_state/js/crw_js_injection_receiver.h" +#include "ios/web/public/web_state/web_state.h" +#include "ui/base/page_transition_types.h" +#include "ui/base/window_open_disposition.h" +#include "url/gurl.h" + +namespace translate { + +namespace { +// The delay we wait in milliseconds before checking whether the translation has +// finished. +// Note: This should be kept in sync with the constant of the same name in +// translate_ios.js. +const int kTranslateStatusCheckDelayMs = 400; +// Language name passed to the Translate element for it to detect the language. +const char kAutoDetectionLanguage[] = "auto"; + +} // namespace + +IOSTranslateDriver::IOSTranslateDriver( + web::WebState* web_state, + web::NavigationManager* navigation_manager, + TranslateManager* translate_manager) + : web::WebStateObserver(web_state), + navigation_manager_(navigation_manager), + translate_manager_(translate_manager->GetWeakPtr()), + page_seq_no_(0), + pending_page_seq_no_(0), + weak_method_factory_(this) { + DCHECK(navigation_manager_); + DCHECK(translate_manager_); + DCHECK(web::WebStateObserver::web_state()); + + CRWJSInjectionReceiver* receiver = web_state->GetJSInjectionReceiver(); + DCHECK(receiver); + + // Create the language detection controller. + JsLanguageDetectionManager* language_detection_manager = + static_cast<JsLanguageDetectionManager*>( + [receiver instanceOfClass:[JsLanguageDetectionManager class]]); + language_detection_controller_.reset(new LanguageDetectionController( + web_state, language_detection_manager, + translate_manager_->translate_client()->GetPrefs())); + language_detection_callback_subscription_ = + language_detection_controller_->RegisterLanguageDetectionCallback( + base::Bind(&IOSTranslateDriver::OnLanguageDetermined, + base::Unretained(this))); + // Create the translate controller. + JsTranslateManager* js_translate_manager = static_cast<JsTranslateManager*>( + [receiver instanceOfClass:[JsTranslateManager class]]); + translate_controller_.reset( + new TranslateController(web_state, js_translate_manager)); + translate_controller_->set_observer(this); +} + +IOSTranslateDriver::~IOSTranslateDriver() { +} + +void IOSTranslateDriver::OnLanguageDetermined( + const LanguageDetectionController::DetectionDetails& details) { + if (!translate_manager_) + return; + translate_manager_->GetLanguageState().LanguageDetermined( + details.adopted_language, true); + + if (web_state()) + translate_manager_->InitiateTranslation(details.adopted_language); +} + +// web::WebStateObserver methods + +void IOSTranslateDriver::NavigationItemCommitted( + const web::LoadCommittedDetails& load_details) { + // 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 (!load_details.is_in_page) { + ++page_seq_no_; + translate_manager_->set_current_seq_no(page_seq_no_); + } + + // TODO(droger): support navigation types, like content/ does. + const bool reload = ui::PageTransitionCoreTypeIs( + load_details.item->GetTransitionType(), ui::PAGE_TRANSITION_RELOAD); + translate_manager_->GetLanguageState().DidNavigate(load_details.is_in_page, + true, reload); +} + +// TranslateDriver methods + +bool IOSTranslateDriver::IsLinkNavigation() { + return navigation_manager_->GetVisibleItem() && + ui::PageTransitionCoreTypeIs( + navigation_manager_->GetVisibleItem()->GetTransitionType(), + ui::PAGE_TRANSITION_LINK); +} + +void IOSTranslateDriver::OnTranslateEnabledChanged() { +} + +void IOSTranslateDriver::OnIsPageTranslatedChanged() { +} + +void IOSTranslateDriver::TranslatePage(int page_seq_no, + const std::string& translate_script, + const std::string& source_lang, + const std::string& target_lang) { + if (page_seq_no != page_seq_no_) + return; // The user navigated away. + source_language_ = source_lang; + target_language_ = target_lang; + pending_page_seq_no_ = page_seq_no; + translate_controller_->InjectTranslateScript(translate_script); +} + +void IOSTranslateDriver::RevertTranslation(int page_seq_no) { + if (page_seq_no != page_seq_no_) + return; // The user navigated away. + translate_controller_->RevertTranslation(); +} + +bool IOSTranslateDriver::IsOffTheRecord() { + return navigation_manager_->GetBrowserState()->IsOffTheRecord(); +} + +const std::string& IOSTranslateDriver::GetContentsMimeType() { + return web_state()->GetContentsMimeType(); +} + +const GURL& IOSTranslateDriver::GetLastCommittedURL() { + return web_state()->GetLastCommittedURL(); +} + +const GURL& IOSTranslateDriver::GetActiveURL() { + web::NavigationItem* item = navigation_manager_->GetVisibleItem(); + if (!item) + return GURL::EmptyGURL(); + return item->GetURL(); +} + +const GURL& IOSTranslateDriver::GetVisibleURL() { + return web_state()->GetVisibleURL(); +} + +bool IOSTranslateDriver::HasCurrentPage() { + return (navigation_manager_->GetVisibleItem() != nullptr); +} + +void IOSTranslateDriver::OpenUrlInNewTab(const GURL& url) { + web::WebState::OpenURLParams params(url, web::Referrer(), 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; + translate::TranslateErrors::Type 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) + translate::ReportTimeToTranslate(translation_time); + // Notify the manage of completion. + translate_manager_->PageTranslated(actual_source_lang, target_lang, + translate_errors); +} + +void IOSTranslateDriver::CheckTranslateStatus( + const std::string& source_language, + const std::string& target_language, + int page_seq_no) { + if (!IsPageValid(page_seq_no)) + return; + translate_controller_->CheckTranslateStatus(); +} + +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(bool success, + double load_time, + double ready_time) { + if (!IsPageValid(pending_page_seq_no_)) + return; + + if (!success) { + translate_manager_->PageTranslated(source_language_, target_language_, + TranslateErrors::INITIALIZATION_ERROR); + return; + } + + translate::ReportTimeToLoad(load_time); + translate::ReportTimeToBeReady(ready_time); + const char kAutoDetectionLanguage[] = "auto"; + std::string source = (source_language_ != translate::kUnknownLanguageCode) + ? source_language_ + : kAutoDetectionLanguage; + translate_controller_->StartTranslation(source_language_, target_language_); + // Check the status of the translation -- after a delay. + base::MessageLoop::current()->PostDelayedTask( + FROM_HERE, base::Bind(&IOSTranslateDriver::CheckTranslateStatus, + weak_method_factory_.GetWeakPtr(), source_language_, + target_language_, pending_page_seq_no_), + base::TimeDelta::FromMilliseconds(kTranslateStatusCheckDelayMs)); +} + +void IOSTranslateDriver::OnTranslateComplete( + bool success, + const std::string& original_language, + double translation_time) { + if (!IsPageValid(pending_page_seq_no_)) + return; + + if (!success) { + // TODO(toyoshim): Check |errorCode| of translate.js and notify it here. + translate_manager_->PageTranslated(source_language_, target_language_, + TranslateErrors::TRANSLATION_ERROR); + } + + TranslationDidSucceed(source_language_, target_language_, + pending_page_seq_no_, original_language, + translation_time); +} + +} // namespace translate diff --git a/components/translate/ios/browser/js_language_detection_manager.h b/components/translate/ios/browser/js_language_detection_manager.h new file mode 100644 index 0000000..b7ca492 --- /dev/null +++ b/components/translate/ios/browser/js_language_detection_manager.h @@ -0,0 +1,39 @@ +// Copyright 2013 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. + +#ifndef COMPONENTS_TRANSLATE_IOS_BROWSER_JS_LANGUAGE_DETECTION_MANAGER_H_ +#define COMPONENTS_TRANSLATE_IOS_BROWSER_JS_LANGUAGE_DETECTION_MANAGER_H_ + +#import <Foundation/Foundation.h> + +#include "base/callback_forward.h" +#include "base/strings/string16.h" +#import "ios/web/public/web_state/js/crw_js_injection_manager.h" + +namespace language_detection { + +// Maximum length of the extracted text returned by |-extractTextContent|. +// Matches desktop implementation. +extern const size_t kMaxIndexChars; + +// Type for the callback called when the buffered text is retrieved. +using BufferedTextCallback = base::Callback<void(const base::string16&)>; + +} // namespace language_detection + +// JsLanguageDetectionManager manages the scripts related to language detection. +@interface JsLanguageDetectionManager : CRWJSInjectionManager + +// Retrieves the cached text content of the page from the JS side. Calls +// |callback| with the page's text contents. The cache is purged on the JS side +// after this call. |callback| must be non null. +- (void)retrieveBufferedTextContent: + (const language_detection::BufferedTextCallback&)callback; + +// Starts detecting the language of the page. +- (void)startLanguageDetection; + +@end + +#endif // COMPONENTS_TRANSLATE_IOS_BROWSER_JS_LANGUAGE_DETECTION_MANAGER_H_ diff --git a/components/translate/ios/browser/js_language_detection_manager.mm b/components/translate/ios/browser/js_language_detection_manager.mm new file mode 100644 index 0000000..d352899 --- /dev/null +++ b/components/translate/ios/browser/js_language_detection_manager.mm @@ -0,0 +1,53 @@ +// Copyright 2013 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 "components/translate/ios/browser/js_language_detection_manager.h" + +#include "base/callback.h" +#include "base/mac/scoped_nsobject.h" +#include "base/strings/string_util.h" +#include "base/strings/sys_string_conversions.h" +#import "ios/web/public/web_state/js/crw_js_base_manager.h" +#import "ios/web/public/web_state/js/crw_js_message_manager.h" + +namespace language_detection { +// Note: This should stay in sync with the constant in language_detection.js. +const size_t kMaxIndexChars = 65535; +} // namespace language_detection + +@implementation JsLanguageDetectionManager + +#pragma mark - Protected methods + +- (NSString*)scriptPath { + return @"language_detection"; +} + +- (NSString*)presenceBeacon { + return @"__gCrWeb.languageDetection"; +} + +- (NSArray*)directDependencies { + return @[ [CRWJSBaseManager class], [CRWJSMessageManager class], ]; +} + +#pragma mark - Public methods + +- (void)startLanguageDetection { + [self evaluate:@"__gCrWeb.languageDetection.detectLanguage()" + stringResultHandler:nil]; +} + +- (void)retrieveBufferedTextContent: + (const language_detection::BufferedTextCallback&)callback { + DCHECK(!callback.is_null()); + // Copy the completion handler so that the block does not capture a reference. + __block language_detection::BufferedTextCallback blockCallback = callback; + [self evaluate:@"__gCrWeb.languageDetection.retrieveBufferedTextContent()" + stringResultHandler:^(NSString* result, NSError*) { + blockCallback.Run(base::SysNSStringToUTF16(result)); + }]; +} + +@end diff --git a/components/translate/ios/browser/js_translate_manager.h b/components/translate/ios/browser/js_translate_manager.h new file mode 100644 index 0000000..e5cac85 --- /dev/null +++ b/components/translate/ios/browser/js_translate_manager.h @@ -0,0 +1,46 @@ +// Copyright 2013 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. + +#ifndef COMPONENTS_TRANSLATE_IOS_BROWSER_JS_TRANSLATE_MANAGER_H_ +#define COMPONENTS_TRANSLATE_IOS_BROWSER_JS_TRANSLATE_MANAGER_H_ + +#import "ios/web/public/web_state/js/crw_js_injection_manager.h" + +#include <string> + +#include "base/time/time.h" + +@class NSString; + +// Manager for the injection of the Translate JavaScript. +// Replicates functionality from TranslateHelper in +// chrome/renderer/translate/translate_helper.cc. +// JsTranslateManager injects the script in the page and calls it, but is not +// responsible for loading it or caching it. +@interface JsTranslateManager : CRWJSInjectionManager + +// The translation script. Must be set before |-inject| is called, and is reset +// after the injection. +@property(nonatomic, copy) NSString* script; + +// Injects JS to constantly check if the translate script is ready and informs +// the Obj-C side when it is. +- (void)injectWaitUntilTranslateReadyScript; + +// After a translation has been initiated, injects JS to check if the +// translation has finished/failed and informs the Obj-C when it is. +- (void)injectTranslateStatusScript; + +// Starts translation of the page from |source| language to |target| language. +// Equivalent to TranslateHelper::StartTranslation(). +- (void)startTranslationFrom:(const std::string&)source + to:(const std::string&)target; + +// Reverts the translation. Assumes that no navigation happened since the page +// has been translated. +- (void)revertTranslation; + +@end + +#endif // COMPONENTS_TRANSLATE_IOS_BROWSER_JS_TRANSLATE_MANAGER_H_ diff --git a/components/translate/ios/browser/js_translate_manager.mm b/components/translate/ios/browser/js_translate_manager.mm new file mode 100644 index 0000000..0431a93 --- /dev/null +++ b/components/translate/ios/browser/js_translate_manager.mm @@ -0,0 +1,84 @@ +// Copyright 2013 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 "components/translate/ios/browser/js_translate_manager.h" + +#import <Foundation/Foundation.h> + +#include "base/logging.h" +#include "base/mac/bundle_locations.h" +#import "base/mac/scoped_nsobject.h" +#include "base/memory/scoped_ptr.h" + +@implementation JsTranslateManager { + base::scoped_nsobject<NSString> _translationScript; +} + +- (NSString*)script { + return _translationScript.get(); +} + +- (void)setScript:(NSString*)script { + // The translation script uses performance.now() for metrics, which is not + // supported except on iOS 8.0. To make the translation script work on these + // iOS versions, add some JavaScript to |script| that defines an + // implementation of performance.now(). + NSString* const kPerformancePlaceholder = + @"var performance = window['performance'] || {};" + @"performance.now = performance['now'] ||" + @"(function () { return Date.now(); });\n"; + script = [kPerformancePlaceholder stringByAppendingString:script]; + // TODO(shreyasv): This leads to some duplicate code from + // CRWJSInjectionManager. Consider refactoring this to its own js injection + // manager. + NSString* path = + [base::mac::FrameworkBundle() pathForResource:@"translate_ios" + ofType:@"js"]; + DCHECK(path); + NSError* error = nil; + NSString* content = [NSString stringWithContentsOfFile:path + encoding:NSUTF8StringEncoding + error:&error]; + DCHECK(!error && [content length]); + script = [script stringByAppendingString:content]; + _translationScript.reset([script copy]); +} + +- (void)injectWaitUntilTranslateReadyScript { + [self.receiver evaluateJavaScript:@"__gCrWeb.translate.checkTranslateReady()" + stringResultHandler:nil]; +} + +- (void)injectTranslateStatusScript { + [self.receiver evaluateJavaScript:@"__gCrWeb.translate.checkTranslateStatus()" + stringResultHandler:nil]; +} + +- (void)startTranslationFrom:(const std::string&)source + to:(const std::string&)target { + NSString* js = + [NSString stringWithFormat:@"cr.googleTranslate.translate('%s','%s')", + source.c_str(), target.c_str()]; + [self.receiver evaluateJavaScript:js stringResultHandler:nil]; +} + +- (void)revertTranslation { + DCHECK([self hasBeenInjected]); + [self.receiver evaluateJavaScript:@"cr.googleTranslate.revert()" + stringResultHandler:nil]; +} + +#pragma mark - +#pragma mark CRWJSInjectionManager methods + +- (NSString*)injectionContent { + DCHECK(_translationScript); + return _translationScript.autorelease(); +} + +- (NSString*)presenceBeacon { + return @"cr.googleTranslate"; +} + +@end diff --git a/components/translate/ios/browser/js_translate_manager_unittest.mm b/components/translate/ios/browser/js_translate_manager_unittest.mm new file mode 100644 index 0000000..e011520 --- /dev/null +++ b/components/translate/ios/browser/js_translate_manager_unittest.mm @@ -0,0 +1,92 @@ +// Copyright 2013 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 "components/translate/ios/browser/js_translate_manager.h" + +#import "base/mac/scoped_nsobject.h" +#include "base/strings/sys_string_conversions.h" +#include "base/time/time.h" +#include "grit/components_resources.h" +#import "ios/web/public/test/crw_test_js_injection_receiver.h" +#import "ios/web/public/test/js_test_util.h" +#import "testing/gtest_mac.h" +#include "testing/platform_test.h" +#include "ui/base/resource/resource_bundle.h" + +using base::Time; +using base::TimeDelta; + +@interface JsTranslateManager (Testing) +- (double)performanceNow; +@end + +@implementation JsTranslateManager (Testing) +// Returns the time in milliseconds. +- (double)performanceNow { + NSString* result = + web::EvaluateJavaScriptAsString(self.receiver, @"performance.now()"); + return [result doubleValue]; +} +@end + +class JsTranslateManagerTest : public PlatformTest { + protected: + JsTranslateManagerTest() { + receiver_.reset([[CRWTestJSInjectionReceiver alloc] init]); + manager_.reset([[JsTranslateManager alloc] initWithReceiver:receiver_]); + base::StringPiece script = + ResourceBundle::GetSharedInstance().GetRawDataResource( + IDR_TRANSLATE_JS); + [manager_ setScript:base::SysUTF8ToNSString(script.as_string() + + "('DummyKey');")]; + } + + bool IsDefined(NSString* name) { + NSString* script = + [NSString stringWithFormat:@"typeof %@ != 'undefined'", name]; + return [web::EvaluateJavaScriptAsString(receiver_, script) isEqual:@"true"]; + } + + base::scoped_nsobject<CRWTestJSInjectionReceiver> receiver_; + base::scoped_nsobject<JsTranslateManager> manager_; +}; + +TEST_F(JsTranslateManagerTest, PerformancePlaceholder) { + [manager_ inject]; + EXPECT_TRUE(IsDefined(@"performance")); + EXPECT_TRUE(IsDefined(@"performance.now")); + + // Check that performance.now returns correct values. + NSTimeInterval intervalInSeconds = 0.3; + double startTime = [manager_ performanceNow]; + [NSThread sleepForTimeInterval:intervalInSeconds]; + double endTime = [manager_ performanceNow]; + double timeElapsed = endTime - startTime; + // The tolerance is high to avoid flake. + EXPECT_NEAR(timeElapsed, intervalInSeconds * 1000, 100); +} + +TEST_F(JsTranslateManagerTest, Inject) { + [manager_ inject]; + EXPECT_TRUE([manager_ hasBeenInjected]); + EXPECT_EQ(nil, [manager_ script]); + // TODO(shreyasv): Switch to the util function in web/ once that CL lands. + __block BOOL block_was_called = NO; + [manager_ evaluate:@"cr.googleTranslate.libReady" + stringResultHandler:^(NSString* result, NSError*) { + block_was_called = YES; + EXPECT_NSEQ(@"false", result); + }]; + // TODO(shreyasv): Move to |WaitUntilCondition| once that is moved to ios/. + const NSTimeInterval kTimeout = 5.0; + Time startTime = Time::Now(); + while (!block_was_called && + (Time::Now() - startTime < TimeDelta::FromSeconds(kTimeout))) { + NSDate* beforeDate = [NSDate dateWithTimeIntervalSinceNow:.01]; + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode + beforeDate:beforeDate]; + } + + EXPECT_TRUE(block_was_called); +} diff --git a/components/translate/ios/browser/language_detection_controller.h b/components/translate/ios/browser/language_detection_controller.h new file mode 100644 index 0000000..28f3efa --- /dev/null +++ b/components/translate/ios/browser/language_detection_controller.h @@ -0,0 +1,94 @@ +// Copyright 2014 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. + +#ifndef COMPONENTS_TRANSLATE_IOS_BROWSER_LANGUAGE_DETECTION_CONTROLLER_H_ +#define COMPONENTS_TRANSLATE_IOS_BROWSER_LANGUAGE_DETECTION_CONTROLLER_H_ + +#include <string> + +#include "base/callback_list.h" +#include "base/gtest_prod_util.h" +#include "base/mac/scoped_nsobject.h" +#include "base/macros.h" +#include "base/memory/weak_ptr.h" +#include "base/prefs/pref_member.h" +#include "base/strings/string16.h" +#include "ios/web/public/web_state/web_state_observer.h" + +class GURL; +@class JsLanguageDetectionManager; +class PrefService; + +namespace base { +class DictionaryValue; +} + +namespace web { +class WebState; +} + +namespace translate { + +class LanguageDetectionController : public web::WebStateObserver { + public: + // Language detection details, passed to language detection callbacks. + struct DetectionDetails { + // The language detected by the content (Content-Language). + std::string content_language; + + // The language written in the lang attribute of the html element. + std::string html_root_language; + + // The adopted language. + std::string adopted_language; + }; + + LanguageDetectionController(web::WebState* web_state, + JsLanguageDetectionManager* manager, + PrefService* prefs); + ~LanguageDetectionController() override; + + // Callback types for language detection events. + typedef base::Callback<void(const DetectionDetails&)> Callback; + typedef base::CallbackList<void(const DetectionDetails&)> CallbackList; + + // Registers a callback for language detection events. + scoped_ptr<CallbackList::Subscription> RegisterLanguageDetectionCallback( + const Callback& callback); + + private: + FRIEND_TEST_ALL_PREFIXES(LanguageDetectionControllerTest, OnTextCaptured); + + // Starts the page language detection and initiates the translation process. + void StartLanguageDetection(); + + // Handles the "languageDetection.textCaptured" javascript command. + // |interacting| is true if the user is currently interacting with the page. + bool OnTextCaptured(const base::DictionaryValue& value, + const GURL& url, + bool interacting); + + // Completion handler used to retrieve the text buffered by the + // JsLanguageDetectionManager. + void OnTextRetrieved(const std::string& http_content_language, + const std::string& html_lang, + const base::string16& text); + + // web::WebStateObserver implementation: + void PageLoaded() override; + void URLHashChanged() override; + void HistoryStateChanged() override; + void WebStateDestroyed() override; + + CallbackList language_detection_callbacks_; + base::scoped_nsobject<JsLanguageDetectionManager> js_manager_; + BooleanPrefMember translate_enabled_; + base::WeakPtrFactory<LanguageDetectionController> weak_method_factory_; + + DISALLOW_COPY_AND_ASSIGN(LanguageDetectionController); +}; + +} // namespace translate + +#endif // COMPONENTS_TRANSLATE_IOS_BROWSER_LANGUAGE_DETECTION_CONTROLLER_H_ diff --git a/components/translate/ios/browser/language_detection_controller.mm b/components/translate/ios/browser/language_detection_controller.mm new file mode 100644 index 0000000..b8ea2be --- /dev/null +++ b/components/translate/ios/browser/language_detection_controller.mm @@ -0,0 +1,146 @@ +// Copyright 2014 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. + +#include "components/translate/ios/browser/language_detection_controller.h" + +#include <string> + +#include "base/bind.h" +#include "base/logging.h" +#include "base/metrics/histogram.h" +#include "base/prefs/pref_member.h" +#include "base/time/time.h" +#include "components/translate/core/common/translate_pref_names.h" +#include "components/translate/core/language_detection/language_detection_util.h" +#import "components/translate/ios/browser/js_language_detection_manager.h" +#include "ios/web/public/string_util.h" +#include "ios/web/public/url_scheme_util.h" +#include "ios/web/public/web_state/web_state.h" + +namespace translate { + +namespace { +// Name for the UMA metric used to track text extraction time. +const char kTranslateCaptureText[] = "Translate.CaptureText"; +// Prefix for the language detection javascript commands. Must be kept in sync +// with language_detection.js. +const char kCommandPrefix[] = "languageDetection"; +} + +LanguageDetectionController::LanguageDetectionController( + web::WebState* web_state, + JsLanguageDetectionManager* manager, + PrefService* prefs) + : web::WebStateObserver(web_state), + js_manager_([manager retain]), + weak_method_factory_(this) { + DCHECK(web::WebStateObserver::web_state()); + DCHECK(js_manager_); + translate_enabled_.Init(prefs::kEnableTranslate, prefs); + web_state->AddScriptCommandCallback( + base::Bind(&LanguageDetectionController::OnTextCaptured, + base::Unretained(this)), + kCommandPrefix); +} + +LanguageDetectionController::~LanguageDetectionController() { +} + +scoped_ptr<LanguageDetectionController::CallbackList::Subscription> +LanguageDetectionController::RegisterLanguageDetectionCallback( + const Callback& callback) { + return language_detection_callbacks_.Add(callback); +} + +void LanguageDetectionController::StartLanguageDetection() { + if (!translate_enabled_.GetValue()) + return; // Translate disabled in preferences. + DCHECK(web_state()); + const GURL& url = web_state()->GetVisibleURL(); + if (!web::UrlHasWebScheme(url) || !web_state()->ContentIsHTML()) + return; + [js_manager_ inject]; + [js_manager_ startLanguageDetection]; +} + +bool LanguageDetectionController::OnTextCaptured( + const base::DictionaryValue& command, + const GURL& url, + bool interacting) { + std::string textCapturedCommand; + if (!command.GetString("command", &textCapturedCommand) || + textCapturedCommand != "languageDetection.textCaptured" || + !command.HasKey("translationAllowed")) { + NOTREACHED(); + return false; + } + bool translation_allowed = false; + command.GetBoolean("translationAllowed", &translation_allowed); + if (!translation_allowed) { + // Translation not allowed by the page. Done processing. + return true; + } + if (!command.HasKey("captureTextTime") || !command.HasKey("htmlLang") || + !command.HasKey("httpContentLanguage")) { + NOTREACHED(); + return false; + } + + int capture_text_time = 0; + command.GetInteger("captureTextTime", &capture_text_time); + UMA_HISTOGRAM_TIMES(kTranslateCaptureText, + base::TimeDelta::FromMillisecondsD(capture_text_time)); + std::string html_lang; + command.GetString("htmlLang", &html_lang); + std::string http_content_language; + command.GetString("httpContentLanguage", &http_content_language); + // If there is no language defined in httpEquiv, use the HTTP header. + if (http_content_language.empty()) + http_content_language = web_state()->GetContentLanguageHeader(); + + [js_manager_ retrieveBufferedTextContent: + base::Bind(&LanguageDetectionController::OnTextRetrieved, + weak_method_factory_.GetWeakPtr(), + http_content_language, html_lang)]; + return true; +} + +void LanguageDetectionController::OnTextRetrieved( + const std::string& http_content_language, + const std::string& html_lang, + const base::string16& text_content) { + std::string language = translate::DeterminePageLanguage( + http_content_language, html_lang, + web::GetStringByClippingLastWord(text_content, + language_detection::kMaxIndexChars), + nullptr /* cld_language */, nullptr /* is_cld_reliable */); + if (language.empty()) + return; // No language detected. + + DetectionDetails details; + details.content_language = http_content_language; + details.html_root_language = html_lang; + details.adopted_language = language; + language_detection_callbacks_.Notify(details); +} + +// web::WebStateObserver implementation: + +void LanguageDetectionController::PageLoaded() { + StartLanguageDetection(); +} + +void LanguageDetectionController::URLHashChanged() { + StartLanguageDetection(); +} + +void LanguageDetectionController::HistoryStateChanged() { + StartLanguageDetection(); +} + +void LanguageDetectionController::WebStateDestroyed() { + web_state()->RemoveScriptCommandCallback(kCommandPrefix); +} + +} // namespace translate diff --git a/components/translate/ios/browser/language_detection_controller_unittest.mm b/components/translate/ios/browser/language_detection_controller_unittest.mm new file mode 100644 index 0000000..e63163e --- /dev/null +++ b/components/translate/ios/browser/language_detection_controller_unittest.mm @@ -0,0 +1,78 @@ +// Copyright 2014 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 "components/translate/ios/browser/language_detection_controller.h" + +#include "base/mac/bind_objc_block.h" +#include "base/prefs/pref_registry_simple.h" +#include "base/prefs/testing_pref_service.h" +#include "base/strings/utf_string_conversions.h" +#include "components/translate/core/common/translate_pref_names.h" +#import "components/translate/ios/browser/js_language_detection_manager.h" +#include "ios/web/public/test/test_web_state.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/platform_test.h" + +@interface MockJsLanguageDetectionManager : JsLanguageDetectionManager +@end + +@implementation MockJsLanguageDetectionManager +- (void)retrieveBufferedTextContent: + (const language_detection::BufferedTextCallback&)callback { + callback.Run(base::UTF8ToUTF16("Some content")); +} +@end + +namespace translate { + +namespace { + +class LanguageDetectionControllerTest : public PlatformTest { + protected: + LanguageDetectionControllerTest() { + prefs_.registry()->RegisterBooleanPref(prefs::kEnableTranslate, true); + + base::scoped_nsobject<MockJsLanguageDetectionManager> js_manager( + [[MockJsLanguageDetectionManager alloc] init]); + controller_.reset(new LanguageDetectionController( + &web_state_, js_manager.get(), &prefs_)); + } + + LanguageDetectionController* controller() { return controller_.get(); } + + private: + TestingPrefServiceSimple prefs_; + web::TestWebState web_state_; + scoped_ptr<LanguageDetectionController> controller_; +}; + +} // namespace + +// Tests that OnTextCaptured() correctly handles messages from the JS side and +// informs the driver. +TEST_F(LanguageDetectionControllerTest, OnTextCaptured) { + const std::string kRootLanguage("en"); + const std::string kContentLanguage("fr"); + + __block bool block_was_called = false; + auto subscription = + controller()->RegisterLanguageDetectionCallback(base::BindBlock( + ^(const LanguageDetectionController::DetectionDetails& details) { + block_was_called = true; + EXPECT_EQ(kRootLanguage, details.html_root_language); + EXPECT_EQ(kContentLanguage, details.content_language); + })); + + base::DictionaryValue command; + command.SetString("command", "languageDetection.textCaptured"); + command.SetBoolean("translationAllowed", true); + command.SetInteger("captureTextTime", 10); + command.SetString("htmlLang", kRootLanguage); + command.SetString("httpContentLanguage", kContentLanguage); + controller()->OnTextCaptured(command, GURL("http://google.com"), false); + + EXPECT_TRUE(block_was_called); +} + +} // namespace translate diff --git a/components/translate/ios/browser/resources/language_detection.js b/components/translate/ios/browser/resources/language_detection.js new file mode 100644 index 0000000..7d54f5f --- /dev/null +++ b/components/translate/ios/browser/resources/language_detection.js @@ -0,0 +1,165 @@ +// Copyright 2013 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. + +__gCrWeb['languageDetection'] = {}; + + +new function() { +/** + * The cache of the text content that was extracted from the page + */ +__gCrWeb.languageDetection.bufferedTextContent = null; + +/** + * The number of active requests that have populated the cache. This is + * incremented every time a call to |__gCrWeb.languageDetection.detectLanguage| + * populates the buffer. This is decremented every time there is a call to + * retrieve the buffer. The buffer is purged when this goes down to 0. + */ +__gCrWeb.languageDetection.activeRequests = 0; + +/** + * Returns true if translation of the page is allowed. + * Translation is not allowed when a "notranslate" meta tag is defined. + * @return {Boolean} true if translation of the page is allowed. + */ +__gCrWeb.languageDetection['translationAllowed'] = function() { + var metaTags = document.getElementsByTagName('meta'); + for (var i = 0; i < metaTags.length; ++i) { + if (metaTags[i].name === 'google') { + if (metaTags[i].content === 'notranslate' || + metaTags[i].getAttribute('value') === 'notranslate') { + return false; + } + } + } + return true; +}; + +/** + * Gets the content of a meta tag by httpEquiv. + * The function is case insensitive. + * @param {String} httpEquiv Value of the "httpEquiv" attribute, has to be + * lower case. + * @return {String} Value of the "content" attribute of the meta tag. + */ +__gCrWeb.languageDetection['getMetaContentByHttpEquiv'] = function(httpEquiv) { + var metaTags = document.getElementsByTagName('meta'); + for (var i = 0; i < metaTags.length; ++i) { + if (metaTags[i].httpEquiv.toLowerCase() === httpEquiv) { + return metaTags[i].content; + } + } + return ''; +}; + +// Used by the |getTextContent| function below. +__gCrWeb.languageDetection['nonTextNodeNames'] = { + 'SCRIPT': 1, + 'NOSCRIPT': 1, + 'STYLE': 1, + 'EMBED': 1, + 'OBJECT': 1 +}; + +/** + * Walks a DOM tree to extract the text content. + * Does not walk into a node when its name is in |nonTextNodeNames|. + * @param {HTMLElement} node The DOM tree + * @param {Integer} maxLen Output will be truncated to |maxLen| + * @return {String} The text content + */ +__gCrWeb.languageDetection['getTextContent'] = function(node, maxLen) { + if (!node || maxLen <= 0) { + return ''; + } + + var txt = ''; + // Formatting and filtering. + if (node.nodeType === document.ELEMENT_NODE) { + // Reject non-text nodes such as scripts. + if (__gCrWeb.languageDetection.nonTextNodeNames[node.nodeName]) { + return ''; + } + if (node.nodeName === 'BR') { + return '\n'; + } + var style = window.getComputedStyle(node); + // Only proceed if the element is visible. + if (style.display === 'none' || style.visibility === 'hidden') { + return ''; + } + // No need to add a line break before |body| as it is the first element. + if (node.nodeName !== 'BODY' && style.display !== 'inline') { + txt = '\n'; + } + } + + if (node.hasChildNodes()) { + for (var childIdx = 0; + childIdx < node.childNodes.length && txt.length < maxLen; + childIdx++) { + txt += __gCrWeb.languageDetection.getTextContent( + node.childNodes[childIdx], maxLen - txt.length); + } + } else if (node.nodeType === document.TEXT_NODE && node.textContent) { + txt += node.textContent.substring(0, maxLen - txt.length); + } + + return txt; +}; + +/** + * Detects if a page has content that needs translation and informs the native + * side. The text content of a page is cached in + * |__gCrWeb.languageDetection.bufferedTextContent| and retrived at a later time + * retrived at a later time directly from the Obj-C side. This is to avoid + * using |invokeOnHost|. + */ +__gCrWeb.languageDetection['detectLanguage'] = function() { + if (!__gCrWeb.languageDetection.translationAllowed()) { + __gCrWeb.message.invokeOnHost({ + 'command': 'languageDetection.textCaptured', + 'translationAllowed': false}); + } else { + // Constant for the maximum length of the extracted text returned by + // |-detectLanguage| to the native side. + // Matches desktop implementation. + // Note: This should stay in sync with the constant in + // js_language_detection_manager.mm . + var kMaxIndexChars = 65535; + var captureBeginTime = new Date(); + __gCrWeb.languageDetection.activeRequests += 1; + __gCrWeb.languageDetection.bufferedTextContent = + __gCrWeb.languageDetection.getTextContent(document.body, + kMaxIndexChars); + var captureTextTime = + (new Date()).getMilliseconds() - captureBeginTime.getMilliseconds(); + var httpContentLanguage = + __gCrWeb.languageDetection.getMetaContentByHttpEquiv( + 'content-language'); + __gCrWeb.message.invokeOnHost({ + 'command': 'languageDetection.textCaptured', + 'translationAllowed': true, + 'captureTextTime': captureTextTime, + 'htmlLang': document.documentElement.lang, + 'httpContentLanguage': httpContentLanguage}); + } +} + +/** + * Retrives the cached text content of a page. Returns it and then purges the + * cache. + */ +__gCrWeb.languageDetection['retrieveBufferedTextContent'] = function() { + var textContent = __gCrWeb.languageDetection.bufferedTextContent; + __gCrWeb.languageDetection.activeRequests -= 1; + if (__gCrWeb.languageDetection.activeRequests == 0) { + __gCrWeb.languageDetection.bufferedTextContent = null; + } + return textContent; +} + +} // End of anonymous object + diff --git a/components/translate/ios/browser/resources/translate_ios.js b/components/translate/ios/browser/resources/translate_ios.js new file mode 100644 index 0000000..356ad28 --- /dev/null +++ b/components/translate/ios/browser/resources/translate_ios.js @@ -0,0 +1,78 @@ +// Copyright 2014 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. + +__gCrWeb['translate'] = {}; + +new function() { +/** + * The delay a wait performed (in milliseconds) before checking whether the + * translation has finished. + * @type {number} + */ +__gCrWeb.translate.TRANSLATE_STATUS_CHECK_DELAY = 400; + +/** + * The delay in milliseconds that we'll wait to check if a page has finished + * loading before attempting a translation. + * @type {number} + */ +__gCrWeb.translate.TRANSLATE_LOAD_CHECK_DELAY = 150; + +/** + * The maximum number of times a check is performed to see if the translate + * library injected in the page is ready. + * @type {number} + */ +__gCrWeb.translate.MAX_TRANSLATE_INIT_CHECK_ATTEMPTS = 5; + +// The number of times polling for the ready status of the translate script has +// been performed. +var translationAttemptCount = 0; + +/** + * Polls every TRANSLATE_LOAD_CHECK_DELAY milliseconds to check if the translate + * script is ready and informs the host when it is. + */ +__gCrWeb.translate['checkTranslateReady'] = function() { + translationAttemptCount += 1; + if (cr.googleTranslate.libReady) { + translationAttemptCount = 0; + __gCrWeb.message.invokeOnHost({ + 'command': 'translate.ready', + 'timeout': false, + 'loadTime': cr.googleTranslate.loadTime, + 'readyTime': cr.googleTranslate.readyTime}); + } else if (translationAttemptCount >= + __gCrWeb.translate.MAX_TRANSLATE_INIT_CHECK_ATTEMPTS) { + __gCrWeb.message.invokeOnHost({ + 'command': 'translate.ready', + 'timeout': true}); + } else { + // The translation is still pending, check again later. + window.setTimeout(__gCrWeb.translate.checkTranslateReady, + __gCrWeb.translate.TRANSLATE_LOAD_CHECK_DELAY); + } +} + +/** + * Polls every TRANSLATE_STATUS_CHECK_DELAY milliseconds to check if translate + * is ready and informs the host when it is. + */ +__gCrWeb.translate['checkTranslateStatus'] = function() { + if (cr.googleTranslate.error) { + __gCrWeb.message.invokeOnHost({'command': 'translate.status', + 'success': false}); + } else if (cr.googleTranslate.finished) { + __gCrWeb.message.invokeOnHost({ + 'command': 'translate.status', + 'success': true, + 'originalPageLanguage': cr.googleTranslate.sourceLang, + 'translationTime': cr.googleTranslate.translationTime}); + } else { + // The translation is still pending, check again later. + window.setTimeout(__gCrWeb.translate.checkTranslateStatus, + __gCrWeb.translate.TRANSLATE_STATUS_CHECK_DELAY); + } +} +} // anonymous function diff --git a/components/translate/ios/browser/translate_controller.h b/components/translate/ios/browser/translate_controller.h new file mode 100644 index 0000000..6cf848e --- /dev/null +++ b/components/translate/ios/browser/translate_controller.h @@ -0,0 +1,106 @@ +// Copyright 2014 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. + +#ifndef COMPONENTS_TRANSLATE_IOS_BROWSER_TRANSLATE_CONTROLLER_H_ +#define COMPONENTS_TRANSLATE_IOS_BROWSER_TRANSLATE_CONTROLLER_H_ + +#include <string> + +#include "base/gtest_prod_util.h" +#include "base/mac/scoped_nsobject.h" +#include "base/macros.h" +#include "base/memory/weak_ptr.h" +#include "ios/web/public/web_state/web_state_observer.h" + +@class JsTranslateManager; +class GURL; + +namespace base { +class DictionaryValue; +} + +namespace web { +class WebState; +} + +namespace translate { + +// TranslateController controls the translation of the page, by injecting the +// translate scripts and monitoring the status. +class TranslateController : public web::WebStateObserver { + public: + // Observer class to monitor the progress of the translation. + class Observer { + public: + // Called when the translate script is ready. + // In case of timeout, |success| is false. + virtual void OnTranslateScriptReady(bool success, + double load_time, + double ready_time) = 0; + + // Called when the translation is complete. + virtual void OnTranslateComplete(bool success, + const std::string& original_language, + double translation_time) = 0; + }; + + TranslateController(web::WebState* web_state, JsTranslateManager* manager); + ~TranslateController() override; + + // Sets the observer. + void set_observer(Observer* observer) { observer_ = observer; } + + // Injects the translate script. + void InjectTranslateScript(const std::string& translate_script); + + // Reverts the translation. + void RevertTranslation(); + + // Starts the translation. Must be called when the translation script is + // ready. + void StartTranslation(const std::string& source_language, + const std::string& target_language); + + // Checks the translation status and calls the observer when it is done. + // This method must be called after StartTranslation(). + void CheckTranslateStatus(); + + // Changes the JsTranslateManager used by this TranslateController. + // Only used for testing. + void SetJsTranslateManagerForTesting(JsTranslateManager* manager) { + js_manager_.reset([manager retain]); + } + + private: + FRIEND_TEST_ALL_PREFIXES(TranslateControllerTest, + OnJavascriptCommandReceived); + FRIEND_TEST_ALL_PREFIXES(TranslateControllerTest, + OnTranslateScriptReadyTimeoutCalled); + FRIEND_TEST_ALL_PREFIXES(TranslateControllerTest, + OnTranslateScriptReadyCalled); + FRIEND_TEST_ALL_PREFIXES(TranslateControllerTest, TranslationSuccess); + FRIEND_TEST_ALL_PREFIXES(TranslateControllerTest, TranslationFailure); + + // Called when a JavaScript command is received. + bool OnJavascriptCommandReceived(const base::DictionaryValue& command, + const GURL& url, + bool interacting); + // Methods to handle specific JavaScript commands. + // Return false if the command is invalid. + bool OnTranslateReady(const base::DictionaryValue& command); + bool OnTranslateComplete(const base::DictionaryValue& command); + + // web::WebStateObserver implementation: + void WebStateDestroyed() override; + + Observer* observer_; + base::scoped_nsobject<JsTranslateManager> js_manager_; + base::WeakPtrFactory<TranslateController> weak_method_factory_; + + DISALLOW_COPY_AND_ASSIGN(TranslateController); +}; + +} // namespace translate + +#endif // COMPONENTS_TRANSLATE_IOS_BROWSER_TRANSLATE_CONTROLLER_H_ diff --git a/components/translate/ios/browser/translate_controller.mm b/components/translate/ios/browser/translate_controller.mm new file mode 100644 index 0000000..6035ba5 --- /dev/null +++ b/components/translate/ios/browser/translate_controller.mm @@ -0,0 +1,140 @@ +// Copyright 2014 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 "components/translate/ios/browser/translate_controller.h" + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/logging.h" +#include "base/strings/sys_string_conversions.h" +#include "base/values.h" +#import "components/translate/ios/browser/js_translate_manager.h" +#include "ios/web/public/web_state/web_state.h" + +namespace translate { + +namespace { +// Prefix for the translate javascript commands. Must be kept in sync with +// translate_ios.js. +const char kCommandPrefix[] = "translate"; +} + +TranslateController::TranslateController(web::WebState* web_state, + JsTranslateManager* manager) + : web::WebStateObserver(web_state), + observer_(nullptr), + js_manager_([manager retain]), + weak_method_factory_(this) { + DCHECK(js_manager_); + DCHECK(web::WebStateObserver::web_state()); + web_state->AddScriptCommandCallback( + base::Bind(&TranslateController::OnJavascriptCommandReceived, + base::Unretained(this)), + kCommandPrefix); +} + +TranslateController::~TranslateController() { +} + +void TranslateController::InjectTranslateScript( + const std::string& translate_script) { + [js_manager_ setScript:base::SysUTF8ToNSString(translate_script)]; + [js_manager_ inject]; + [js_manager_ injectWaitUntilTranslateReadyScript]; +} + +void TranslateController::RevertTranslation() { + [js_manager_ revertTranslation]; +} + +void TranslateController::StartTranslation(const std::string& source_language, + const std::string& target_language) { + [js_manager_ startTranslationFrom:source_language to:target_language]; +} + +void TranslateController::CheckTranslateStatus() { + [js_manager_ injectTranslateStatusScript]; +} + +bool TranslateController::OnJavascriptCommandReceived( + const base::DictionaryValue& command, + const GURL& url, + bool interacting) { + const base::Value* value = nullptr; + command.Get("command", &value); + if (!value) { + return false; + } + + std::string out_string; + value->GetAsString(&out_string); + if (out_string == "translate.ready") + return OnTranslateReady(command); + else if (out_string == "translate.status") + return OnTranslateComplete(command); + + NOTREACHED(); + return false; +} + +bool TranslateController::OnTranslateReady( + const base::DictionaryValue& command) { + if (!command.HasKey("timeout")) { + NOTREACHED(); + return false; + } + + bool timeout = false; + double load_time = 0.; + double ready_time = 0.; + + command.GetBoolean("timeout", &timeout); + if (!timeout) { + if (!command.HasKey("loadTime") || !command.HasKey("readyTime")) { + NOTREACHED(); + return false; + } + command.GetDouble("loadTime", &load_time); + command.GetDouble("readyTime", &ready_time); + } + if (observer_) + observer_->OnTranslateScriptReady(!timeout, load_time, ready_time); + return true; +} + +bool TranslateController::OnTranslateComplete( + const base::DictionaryValue& command) { + if (!command.HasKey("success")) { + NOTREACHED(); + return false; + } + + bool success = false; + std::string original_language; + double translation_time = 0.; + + command.GetBoolean("success", &success); + if (success) { + if (!command.HasKey("originalPageLanguage") || + !command.HasKey("translationTime")) { + NOTREACHED(); + return false; + } + command.GetString("originalPageLanguage", &original_language); + command.GetDouble("translationTime", &translation_time); + } + + if (observer_) + observer_->OnTranslateComplete(success, original_language, + translation_time); + return true; +} + +// web::WebStateObserver implementation. + +void TranslateController::WebStateDestroyed() { + web_state()->RemoveScriptCommandCallback(kCommandPrefix); +} + +} // namespace translate diff --git a/components/translate/ios/browser/translate_controller_unittest.mm b/components/translate/ios/browser/translate_controller_unittest.mm new file mode 100644 index 0000000..0e572b9 --- /dev/null +++ b/components/translate/ios/browser/translate_controller_unittest.mm @@ -0,0 +1,142 @@ +// Copyright 2014 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 "components/translate/ios/browser/translate_controller.h" + +#include "base/values.h" +#import "components/translate/ios/browser/js_translate_manager.h" +#include "ios/web/public/test/test_web_state.h" +#include "testing/platform_test.h" +#import "third_party/ocmock/OCMock/OCMock.h" +#include "url/gurl.h" + +namespace translate { + +class TranslateControllerTest : public PlatformTest, + public TranslateController::Observer { + protected: + TranslateControllerTest() + : test_web_state_(new web::TestWebState), + success_(false), + ready_time_(0), + load_time_(0), + translation_time_(0), + on_script_ready_called_(false), + on_translate_complete_called_(false) { + mock_js_translate_manager_.reset( + [[OCMockObject niceMockForClass:[JsTranslateManager class]] retain]); + translate_controller_.reset(new TranslateController( + test_web_state_.get(), mock_js_translate_manager_)); + translate_controller_->set_observer(this); + } + + // TranslateController::Observer methods. + void OnTranslateScriptReady(bool success, + double load_time, + double ready_time) override { + on_script_ready_called_ = true; + success_ = success; + load_time_ = load_time; + ready_time_ = ready_time; + } + + void OnTranslateComplete(bool success, + const std::string& original_language, + double translation_time) override { + on_translate_complete_called_ = true; + success_ = success; + original_language_ = original_language; + translation_time_ = translation_time; + } + + scoped_ptr<web::TestWebState> test_web_state_; + base::scoped_nsobject<id> mock_js_translate_manager_; + scoped_ptr<TranslateController> translate_controller_; + bool success_; + double ready_time_; + double load_time_; + std::string original_language_; + double translation_time_; + bool on_script_ready_called_; + bool on_translate_complete_called_; +}; + +// Tests that OnJavascriptCommandReceived() returns false to malformed commands. +TEST_F(TranslateControllerTest, OnJavascriptCommandReceived) { + base::DictionaryValue malformed_command; + EXPECT_FALSE(translate_controller_->OnJavascriptCommandReceived( + malformed_command, GURL("http://google.com"), false)); +} + +// Tests that OnTranslateScriptReady() is called when a timeout message is +// recieved from the JS side. +TEST_F(TranslateControllerTest, OnTranslateScriptReadyTimeoutCalled) { + base::DictionaryValue command; + command.SetString("command", "translate.ready"); + command.SetBoolean("timeout", true); + command.SetDouble("loadTime", .0); + command.SetDouble("readyTime", .0); + EXPECT_TRUE(translate_controller_->OnJavascriptCommandReceived( + command, GURL("http://google.com"), false)); + EXPECT_TRUE(on_script_ready_called_); + EXPECT_FALSE(on_translate_complete_called_); + EXPECT_FALSE(success_); +} + +// Tests that OnTranslateScriptReady() is called with the right parameters when +// a |translate.ready| message is recieved from the JS side. +TEST_F(TranslateControllerTest, OnTranslateScriptReadyCalled) { + // Arbitrary values. + double some_load_time = 23.1; + double some_ready_time = 12.2; + + base::DictionaryValue command; + command.SetString("command", "translate.ready"); + command.SetBoolean("timeout", false); + command.SetDouble("loadTime", some_load_time); + command.SetDouble("readyTime", some_ready_time); + EXPECT_TRUE(translate_controller_->OnJavascriptCommandReceived( + command, GURL("http://google.com"), false)); + EXPECT_TRUE(on_script_ready_called_); + EXPECT_FALSE(on_translate_complete_called_); + EXPECT_TRUE(success_); + EXPECT_EQ(some_load_time, load_time_); + EXPECT_EQ(some_ready_time, ready_time_); +} + +// Tests that OnTranslateComplete() is called with the right parameters when a +// |translate.status| message is recieved from the JS side. +TEST_F(TranslateControllerTest, TranslationSuccess) { + // Arbitrary values. + std::string some_original_language("en"); + double some_translation_time = 12.9; + + base::DictionaryValue command; + command.SetString("command", "translate.status"); + command.SetBoolean("success", true); + command.SetString("originalPageLanguage", some_original_language); + command.SetDouble("translationTime", some_translation_time); + EXPECT_TRUE(translate_controller_->OnJavascriptCommandReceived( + command, GURL("http://google.com"), false)); + EXPECT_FALSE(on_script_ready_called_); + EXPECT_TRUE(on_translate_complete_called_); + EXPECT_TRUE(success_); + EXPECT_EQ(some_original_language, original_language_); + EXPECT_EQ(some_translation_time, translation_time_); +} + +// Tests that OnTranslateComplete() is called with the right parameters when a +// |translate.status| message is recieved from the JS side. +TEST_F(TranslateControllerTest, TranslationFailure) { + base::DictionaryValue command; + command.SetString("command", "translate.status"); + command.SetBoolean("success", false); + EXPECT_TRUE(translate_controller_->OnJavascriptCommandReceived( + command, GURL("http://google.com"), false)); + EXPECT_FALSE(on_script_ready_called_); + EXPECT_TRUE(on_translate_complete_called_); + EXPECT_FALSE(success_); +} + +} // namespace translate |