chromium/components/translate/content/android/java/src/org/chromium/components/translate/TranslateOptions.java

// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.components.translate;

import android.text.TextUtils;

import androidx.annotation.IntDef;
import androidx.annotation.Nullable;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

/**
 * A class that keeps the state of the different translation options and
 * languages.
 */
public class TranslateOptions {
    /**
     * A container for Language Code and it's translated representation and it's native UMA
     * specific hashcode.
     * For example for Spanish when viewed from a French locale, this will contain es, Espagnol,
     *EspaƱol, 114573335
     **/
    public static class TranslateLanguageData {
        public final String mLanguageCode;
        public final String mLanguageRepresentation;
        // TODO(crbug.com/40266152): Remove |mLanguageUMAHashCode| as these hashes
        // are no longer used.
        public final Integer mLanguageUMAHashCode;

        public TranslateLanguageData(
                String languageCode, String languageRepresentation, Integer uMAhashCode) {
            assert languageCode != null;
            assert languageRepresentation != null;
            mLanguageCode = languageCode;
            mLanguageRepresentation = languageRepresentation;
            mLanguageUMAHashCode = uMAhashCode;
        }

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof TranslateLanguageData)) return false;
            TranslateLanguageData other = (TranslateLanguageData) obj;
            return this.mLanguageCode.equals(other.mLanguageCode)
                    && this.mLanguageRepresentation.equals(other.mLanguageRepresentation)
                    && this.mLanguageUMAHashCode.equals(other.mLanguageUMAHashCode);
        }

        @Override
        public int hashCode() {
            return (mLanguageCode + mLanguageRepresentation).hashCode();
        }

        @Override
        public String toString() {
            return "mLanguageCode:"
                    + mLanguageCode
                    + " - mLanguageRepresentation "
                    + mLanguageRepresentation
                    + " - mLanguageUMAHashCode "
                    + mLanguageUMAHashCode;
        }
    }

    // Values must be numerated from 0 and can't have gaps
    // (they're used for indexing mOptions).
    @IntDef({Type.NEVER_LANGUAGE, Type.NEVER_DOMAIN, Type.ALWAYS_LANGUAGE})
    @Retention(RetentionPolicy.SOURCE)
    public @interface Type {
        int NEVER_LANGUAGE = 0;
        int NEVER_DOMAIN = 1;
        int ALWAYS_LANGUAGE = 2;

        int NUM_ENTRIES = 3;
    }

    private String mSourceLanguageCode;
    private String mTargetLanguageCode;

    private final ArrayList<TranslateLanguageData> mAllLanguages;
    @Nullable private String[] mContentLanguagesCodes;

    // Language code to UI display language name map Conceptually final
    private Map<String, String> mCodeToRepresentation;

    // Will reflect the state before the object was ever modified
    private final boolean[] mOriginalOptions;

    private final String mOriginalSourceLanguageCode;
    private final String mOriginalTargetLanguageCode;
    private final boolean mTriggeredFromMenu;

    private final boolean[] mOptions;

    private TranslateOptions(
            String sourceLanguageCode,
            String targetLanguageCode,
            ArrayList<TranslateLanguageData> allLanguages,
            String[] contentLanguages,
            boolean neverLanguage,
            boolean neverDomain,
            boolean alwaysLanguage,
            boolean triggeredFromMenu,
            boolean[] originalOptions) {
        assert Type.NUM_ENTRIES == 3;
        mOptions = new boolean[Type.NUM_ENTRIES];
        mOptions[Type.NEVER_LANGUAGE] = neverLanguage;
        mOptions[Type.NEVER_DOMAIN] = neverDomain;
        mOptions[Type.ALWAYS_LANGUAGE] = alwaysLanguage;

        mOriginalOptions = originalOptions == null ? mOptions.clone() : originalOptions.clone();

        mSourceLanguageCode = sourceLanguageCode;
        mTargetLanguageCode = targetLanguageCode;
        mOriginalSourceLanguageCode = mSourceLanguageCode;
        mOriginalTargetLanguageCode = mTargetLanguageCode;
        mTriggeredFromMenu = triggeredFromMenu;

        mAllLanguages = allLanguages;
        mContentLanguagesCodes = contentLanguages;
        mCodeToRepresentation = new HashMap<String, String>();
        for (TranslateLanguageData language : allLanguages) {
            mCodeToRepresentation.put(language.mLanguageCode, language.mLanguageRepresentation);
        }
    }

    /** Creates a TranslateOptions by the given data. */
    public static TranslateOptions create(
            String sourceLanguageCode,
            String targetLanguageCode,
            String[] languages,
            String[] codes,
            boolean neverLanguage,
            boolean neverDomain,
            boolean alwaysTranslate,
            boolean triggeredFromMenu,
            int[] hashCodes,
            String[] contentLanguagesCodes) {
        assert languages.length == codes.length;

        ArrayList<TranslateLanguageData> languageList = new ArrayList<TranslateLanguageData>();
        for (int i = 0; i < languages.length; ++i) {
            Integer hashCode = hashCodes != null ? Integer.valueOf(hashCodes[i]) : null;
            languageList.add(new TranslateLanguageData(codes[i], languages[i], hashCode));
        }

        return new TranslateOptions(
                sourceLanguageCode,
                targetLanguageCode,
                languageList,
                contentLanguagesCodes,
                neverLanguage,
                neverDomain,
                alwaysTranslate,
                triggeredFromMenu,
                null);
    }

    /** Returns a copy of the current instance. */
    TranslateOptions copy() {
        return new TranslateOptions(
                mSourceLanguageCode,
                mTargetLanguageCode,
                mAllLanguages,
                mContentLanguagesCodes,
                mOptions[Type.NEVER_LANGUAGE],
                mOptions[Type.NEVER_DOMAIN],
                mOptions[Type.ALWAYS_LANGUAGE],
                mTriggeredFromMenu,
                mOriginalOptions);
    }

    /** Updates content languages. */
    public void updateContentLanguages(String[] contentLanguagesCodes) {
        this.mContentLanguagesCodes = contentLanguagesCodes;
    }

    public String sourceLanguageName() {
        return getRepresentationFromCode(mSourceLanguageCode);
    }

    public String targetLanguageName() {
        return getRepresentationFromCode(mTargetLanguageCode);
    }

    public String sourceLanguageCode() {
        return mSourceLanguageCode;
    }

    public String targetLanguageCode() {
        return mTargetLanguageCode;
    }

    public boolean triggeredFromMenu() {
        return mTriggeredFromMenu;
    }

    public boolean optionsChanged() {
        return (!mSourceLanguageCode.equals(mOriginalSourceLanguageCode))
                || (!mTargetLanguageCode.equals(mOriginalTargetLanguageCode))
                || (mOptions[Type.NEVER_LANGUAGE] != mOriginalOptions[Type.NEVER_LANGUAGE])
                || (mOptions[Type.NEVER_DOMAIN] != mOriginalOptions[Type.NEVER_DOMAIN])
                || (mOptions[Type.ALWAYS_LANGUAGE] != mOriginalOptions[Type.ALWAYS_LANGUAGE]);
    }

    public List<TranslateLanguageData> allLanguages() {
        return mAllLanguages;
    }

    @Nullable
    public String[] contentLanguages() {
        return mContentLanguagesCodes;
    }

    public boolean getTranslateState(@Type int type) {
        return mOptions[type];
    }

    public boolean setSourceLanguage(String languageCode) {
        boolean canSet = canSetLanguage(languageCode, mTargetLanguageCode);
        if (canSet) mSourceLanguageCode = languageCode;
        return canSet;
    }

    public boolean setTargetLanguage(String languageCode) {
        boolean canSet = canSetLanguage(mSourceLanguageCode, languageCode);
        if (canSet) mTargetLanguageCode = languageCode;
        return canSet;
    }

    /**
     * Sets the new state of never translate domain.
     *
     * @return true if the toggling was possible
     */
    public void toggleNeverTranslateDomainState(boolean value) {
        mOptions[Type.NEVER_DOMAIN] = value;
    }

    /**
     * Sets the new state of never translate language.
     *
     * @return true if the toggling was possible
     */
    public void toggleNeverTranslateLanguageState(boolean value) {
        // Ensure AlwaysTranslate is disabled if enabling NeverTranslate.
        if (mOptions[Type.ALWAYS_LANGUAGE] && value) {
            toggleAlwaysTranslateLanguageState(false);
        }
        mOptions[Type.NEVER_LANGUAGE] = value;
    }

    /**
     * Sets the new state of never translate a language pair.
     *
     * @return true if the toggling was possible
     */
    public void toggleAlwaysTranslateLanguageState(boolean value) {
        // Ensure NeverTranslate is disabled if enabling AlwaysTranslate.
        if (mOptions[Type.NEVER_LANGUAGE] && value) {
            toggleNeverTranslateLanguageState(false);
        }
        mOptions[Type.ALWAYS_LANGUAGE] = value;
    }

    /**
     * Gets the language's translated representation from a given language code.
     * @param languageCode ISO code for the language
     * @return The translated representation of the language, or "" if not found.
     */
    public String getRepresentationFromCode(String languageCode) {
        return isValidLanguageCode(languageCode) ? mCodeToRepresentation.get(languageCode) : "";
    }

    /**
     * Gets the language's native representation from a given language code.
     * Only for content languages.
     * @param languageCode ISO code for the language
     * @return The native representation of the language.
     */
    public String getNativeRepresentationFromCode(String languageCode) {
        if (isValidLanguageCode(languageCode)) {
            Locale locale = Locale.forLanguageTag(languageCode);
            return locale.getDisplayName(locale);
        }
        return "";
    }

    private boolean isValidLanguageCode(String languageCode) {
        return !TextUtils.isEmpty(languageCode) && mCodeToRepresentation.containsKey(languageCode);
    }

    private boolean canSetLanguage(String sourceCode, String targetCode) {
        return isValidLanguageCode(sourceCode) && isValidLanguageCode(targetCode);
    }

    @Override
    public String toString() {
        return new StringBuilder()
                .append(sourceLanguageCode())
                .append(" -> ")
                .append(targetLanguageCode())
                .append(" - ")
                .append("Never Language:")
                .append(mOptions[Type.NEVER_LANGUAGE])
                .append(" Always Language:")
                .append(mOptions[Type.ALWAYS_LANGUAGE])
                .append(" Never Domain:")
                .append(mOptions[Type.NEVER_DOMAIN])
                .toString();
    }
}