// Copyright 2016 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.
package org.chromium.chrome.browser.contextualsearch;
import android.content.Context;
import android.text.TextUtils;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.ui.UiUtils;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
/**
* Controls how Translation One-box triggering is handled for the {@link ContextualSearchManager}.
*/
public class ContextualSearchTranslateController {
private static final int LOCALE_MIN_LENGTH = 2;
private final ChromeActivity mActivity;
private final ContextualSearchPolicy mPolicy;
private final ContextualSearchTranslateInterface mHost;
// Cached native language data for translation;
private String mTranslateServiceTargetLanguage;
private String mAcceptLanguages;
ContextualSearchTranslateController(ChromeActivity activity, ContextualSearchPolicy policy,
ContextualSearchTranslateInterface hostInterface) {
mActivity = activity;
mPolicy = policy;
mHost = hostInterface;
}
/**
* Force translation from the given language for the current search request,
* unless disabled by experiment. Also log whenever conditions are right to translate.
* @param searchRequest The search request to force translation upon.
* @param sourceLanguage The language to translate from, or an empty string if not known.
*/
void forceTranslateIfNeeded(ContextualSearchRequest searchRequest,
String sourceLanguage) {
if (!mPolicy.isTranslationEnabled()) return;
// Force translation if not disabled and server controlled or client logic says required.
boolean doForceTranslate = !mPolicy.isForceTranslationOneboxDisabled()
&& (ContextualSearchFieldTrial.isServerControlledOneboxEnabled()
|| !TextUtils.isEmpty(sourceLanguage)
&& mPolicy.needsTranslation(
sourceLanguage, getReadableLanguages()));
if (doForceTranslate && searchRequest != null) {
searchRequest.forceTranslation(
sourceLanguage, mPolicy.bestTargetLanguage(getProficientLanguageList()));
}
// Log that conditions were right for translation, even though it may be disabled
// for an experiment so we can compare with the counter factual data.
ContextualSearchUma.logTranslateOnebox(doForceTranslate);
}
/**
* Force auto-detect translation for the current search request unless disabled by experiment.
* Also log that conditions are right to translate.
* @param searchRequest The search request to force translation upon.
*/
void forceAutoDetectTranslateUnlessDisabled(ContextualSearchRequest searchRequest) {
// Always trigger translation using auto-detect when we're not resolving,
// unless disabled by policy.
if (!mPolicy.isTranslationEnabled()) return;
boolean shouldAutoDetectTranslate = !mPolicy.isAutoDetectTranslationOneboxDisabled();
if (shouldAutoDetectTranslate && searchRequest != null) {
// The translation one-box won't actually show when the source text ends up being
// the same as the target text, so we err on over-triggering.
searchRequest.forceAutoDetectTranslation(
mPolicy.bestTargetLanguage(getProficientLanguageList()));
}
// Log that conditions were right for translation, even though it may be disabled
// for an experiment so we can compare with the counter factual data.
ContextualSearchUma.logTranslateOnebox(shouldAutoDetectTranslate);
}
/**
* Caches all the native translate language info, so we can avoid repeated JNI calls.
*/
void cacheNativeTranslateData() {
if (!mPolicy.isTranslationEnabled()) return;
if (!mPolicy.isForceTranslationOneboxDisabled()) {
getNativeTranslateServiceTargetLanguage();
getNativeAcceptLanguages();
}
}
/**
* Gets the list of readable languages for the current user, with the first
* item in the list being the user's primary language (according to the Translate Service).
* We assume that the user can read all languages that they can write.
* @return The {@link List} of languages the user understands with their primary language first.
*/
private List<String> getReadableLanguages() {
// Using LinkedHashSet keeps the entries both unique and ordered.
LinkedHashSet<String> uniqueLanguages = getProficientLanguages();
// Add the accept languages to the end, since they are a weaker hint than
// the proficient languages.
List<String> acceptLanguages = getAcceptLanguages();
for (String accept : acceptLanguages) {
if (isValidLocale(accept)) uniqueLanguages.add(trimLocaleToLanguage(accept));
}
return new ArrayList<String>(uniqueLanguages);
}
/**
* Gets the list of languages that the current user is proficient using.
* The list produced is based on the Translation-Service's target language, supplemented
* with the user's IME keyboard locales.
* @return An ordered {@link List} of languages the user is proficient using.
*/
private ArrayList<String> getProficientLanguageList() {
return new ArrayList<String>(getProficientLanguages());
}
/**
* Similar to {@link #getProficientLanguageList} except the the result is provided in
* a {@link LinkedHashSet} to provide access to a unique ordered list.
* @return a {@link LinkedHashSet} of languages the user is proficient using.
*/
private LinkedHashSet<String> getProficientLanguages() {
LinkedHashSet<String> uniqueLanguages = new LinkedHashSet<String>();
// The primary language, according to the translation-service, always comes first.
uniqueLanguages.add(trimLocaleToLanguage(getNativeTranslateServiceTargetLanguage()));
// Merge in the IME locales, if possible.
if (!ContextualSearchFieldTrial.isKeyboardLanguagesForTranslationDisabled()) {
Context context = mActivity.getApplicationContext();
if (context != null) {
for (String locale : UiUtils.getIMELocales(context)) {
if (isValidLocale(locale)) uniqueLanguages.add(trimLocaleToLanguage(locale));
}
}
}
return uniqueLanguages;
}
/**
* Gets the list of accept languages for this user.
* @return The {@link List} of languages the user understands or does not want translated.
*/
private List<String> getAcceptLanguages() {
List<String> result = new ArrayList<String>();
if (!ContextualSearchFieldTrial.isAcceptLanguagesForTranslationDisabled()) {
String acceptLanguages = getNativeAcceptLanguages();
if (!TextUtils.isEmpty(acceptLanguages)) {
for (String language : acceptLanguages.split(",")) {
result.add(language);
}
}
}
return result;
}
/**
* @return Whether the given locale appears to be valid.
*/
private boolean isValidLocale(String locale) {
return !TextUtils.isEmpty(locale) && locale.length() >= LOCALE_MIN_LENGTH;
}
/**
* Converts a given locale to a language code.
* @param locale The locale string, which must have a length of at least 2.
* @return The given locale as a language code.
*/
private String trimLocaleToLanguage(String locale) {
// TODO(donnd): use getScript or getLanguageTag (both API 21), or some other standard way to
// strip the country, instead of hard-coding the two character language code.
// TODO(donnd): Shouldn't getLanguage() do this?
String trimmedLocale = locale.substring(0, LOCALE_MIN_LENGTH);
return new Locale(trimmedLocale).getLanguage();
}
/**
* @return The accept-languages string from the cache or from native code (when not cached).
*/
protected String getNativeAcceptLanguages() {
if (mAcceptLanguages == null) {
mAcceptLanguages = mHost.getAcceptLanguages();
}
return mAcceptLanguages;
}
/**
* @return The Translate Service's target language string from the cache or from
* native code (when not cached).
*/
protected String getNativeTranslateServiceTargetLanguage() {
if (mTranslateServiceTargetLanguage == null) {
mTranslateServiceTargetLanguage = mHost.getTranslateServiceTargetLanguage();
}
return mTranslateServiceTargetLanguage;
}
}