chromium/chrome/android/java/src/org/chromium/chrome/browser/ChromeLocalizationUtils.java

// Copyright 2019 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.chrome.browser;

import android.os.LocaleList;
import android.text.TextUtils;

import androidx.annotation.IntDef;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.ContextUtils;
import org.chromium.base.LocaleUtils;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.language.AppLocaleUtils;
import org.chromium.chrome.browser.language.GlobalAppLocaleController;
import org.chromium.ui.base.LocalizationUtils;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Locale;

/** This class provides the locale related methods for Chrome. */
public class ChromeLocalizationUtils {
    // Constants used to log UI language availability. Must stay in sync with values in the
    // LanguageUsage.UI.Available enum. These values are persisted to logs. Entries should
    // not be renumbered and numeric values should never be reused.
    @IntDef({
        UiAvailableTypes.TOP_AVAILABLE,
        UiAvailableTypes.ONLY_DEFAULT_AVAILABLE,
        UiAvailableTypes.NONE_AVAILABLE,
        UiAvailableTypes.OVERRIDDEN
    })
    @Retention(RetentionPolicy.SOURCE)
    @interface UiAvailableTypes {
        int TOP_AVAILABLE = 0;
        int ONLY_DEFAULT_AVAILABLE = 1;
        int NONE_AVAILABLE = 2;
        int OVERRIDDEN = 3;
        int NUM_ENTRIES = 4;
    }

    // Constants used to log the UI language correctness. Must stay in sync with values in the
    // LanguageUsage.UI.Android.Correctness enum. These values are persisted to logs. Entries
    // should not be renumbered and numeric values should never be reused.
    @IntDef({
        UiCorrectTypes.CORRECT,
        UiCorrectTypes.INCORRECT,
        UiCorrectTypes.NOT_AVAILABLE,
        UiCorrectTypes.ONLY_JAVA_CORRECT
    })
    @Retention(RetentionPolicy.SOURCE)
    @interface UiCorrectTypes {
        int CORRECT = 0;
        int INCORRECT = 1;
        int NOT_AVAILABLE = 2;
        int ONLY_JAVA_CORRECT = 3;
        int NUM_ENTRIES = 4;
    }

    // Constants used to log the locale update status. Must stay in sync with values in the
    // LanguageUsage.UI.Android.LocaleUpdateStatus enum. These values are persisted to logs. Entries
    // should not be renumbered and numeric values should never be reused.
    @IntDef({
        LocaleUpdateStatus.NO_CHANGE,
        LocaleUpdateStatus.OVERRIDDEN_TOP_CHANGED,
        LocaleUpdateStatus.OVERRIDDEN_OTHERS_CHANGED,
        LocaleUpdateStatus.NO_OVERRIDE_TOP_CHANGED,
        LocaleUpdateStatus.NO_OVERRIDE_OTHERS_CHANGED,
        LocaleUpdateStatus.FIRST_RUN
    })
    @Retention(RetentionPolicy.SOURCE)
    @interface LocaleUpdateStatus {
        int NO_CHANGE = 0;
        int OVERRIDDEN_TOP_CHANGED = 1;
        int OVERRIDDEN_OTHERS_CHANGED = 2;
        int NO_OVERRIDE_TOP_CHANGED = 3;
        int NO_OVERRIDE_OTHERS_CHANGED = 4;
        int FIRST_RUN = 5;
        int NUM_ENTRIES = 6;
    }

    /**
     * @return the current Chromium locale used to display UI elements.
     *
     * This matches what the Android framework resolves localized string resources to, using the
     * system locale and the application's resources. For example, if the system uses a locale
     * that is not supported by Chromium resources (e.g. 'fur-rIT'), Android will likely fallback
     * to 'en-rUS' strings when Resources.getString() is called, and this method will return the
     * matching Chromium name (i.e. 'en-US').
     *
     * Using this value is necessary to ensure that the strings accessed from the locale .pak files
     * from C++ match the resources displayed by the Java-based UI views.
     */
    public static String getJavaUiLocale() {
        return ContextUtils.getApplicationContext()
                .getResources()
                .getString(R.string.current_detected_ui_locale_name);
    }

    /**
     * Records the status of the current UI language under "LanguageUsage.UI.Android.*". Tracks if
     * the Android system language is available and if the Chromium UI language is correct.
     *
     * On N+ both the top Android language and default Android language are checked for
     * availability. The default language is the one used by the JVM for localization. These will be
     * different if the top Android is not available for localization in Chromium. Otherwise they
     * are the same.
     *
     * For correctness both the Java and native UI languages are checked. These can be different if
     * an override language is set and Play Store hygiene has not run.
     */
    public static void recordUiLanguageStatus() {
        String defaultLanguage = LocaleUtils.toBaseLanguage(Locale.getDefault().toLanguageTag());

        // The default locale is the first Android locale with translated Chromium resources. On N+
        // the top system language can be retrieved, even if it is not an option for Chromium's UI.
        String topAndroidLanguage =
                LocaleUtils.toBaseLanguage(LocaleList.getDefault().get(0).toLanguageTag());

        boolean isDefaultLanguageAvailable = AppLocaleUtils.isSupportedUiLanguage(defaultLanguage);
        boolean isTopAndroidLanguageAvailable =
                AppLocaleUtils.isSupportedUiLanguage(topAndroidLanguage);

        // The java and native UI languages can be different if the native language pack is not
        // correctly installed through the Play Store.
        String javaUiLanguage = LocaleUtils.toBaseLanguage(getJavaUiLocale());
        String nativeUiLanguage = LocaleUtils.toBaseLanguage(LocalizationUtils.getNativeUiLocale());
        boolean isJavaUiCorrect = TextUtils.equals(defaultLanguage, javaUiLanguage);
        boolean isNativeUiCorrect = TextUtils.equals(defaultLanguage, nativeUiLanguage);

        // The Android Chromium UI language can be overridden at the device level by users.
        boolean isOverridden = GlobalAppLocaleController.getInstance().isOverridden();

        @UiAvailableTypes
        int availableStatus =
                getUiAvailabilityStatus(
                        isOverridden, isTopAndroidLanguageAvailable, isDefaultLanguageAvailable);
        RecordHistogram.recordEnumeratedHistogram(
                "LanguageUsage.UI.Android.Availability",
                availableStatus,
                UiAvailableTypes.NUM_ENTRIES);

        boolean noLanguageAvailable = !isTopAndroidLanguageAvailable && !isDefaultLanguageAvailable;
        @UiCorrectTypes
        int correctStatus =
                getUiCorrectnessStatus(noLanguageAvailable, isJavaUiCorrect, isNativeUiCorrect);
        RecordHistogram.recordEnumeratedHistogram(
                "LanguageUsage.UI.Android.Correctness", correctStatus, UiCorrectTypes.NUM_ENTRIES);

        if (isOverridden) {
            @UiCorrectTypes
            int overrideStatus = getOverrideUiCorrectStatus(isJavaUiCorrect, isNativeUiCorrect);
            RecordHistogram.recordEnumeratedHistogram(
                    "LanguageUsage.UI.Android.Correctness.Override",
                    overrideStatus,
                    UiCorrectTypes.NUM_ENTRIES);
        } else {
            @UiCorrectTypes
            int noOverrideStatus =
                    getNoOverrideUiCorrectStatus(noLanguageAvailable, isJavaUiCorrect);
            RecordHistogram.recordEnumeratedHistogram(
                    "LanguageUsage.UI.Android.Correctness.NoOverride",
                    noOverrideStatus,
                    UiCorrectTypes.NUM_ENTRIES);
        }
    }

    /**
     * Return the status of Android system languages for use as the Chromium UI language.
     * The default language is the one used by the JVM for localization. This will be different from
     * the top Android language if the top Android language is not available for localization in
     * Chromium. See {@link LocaleList#getDefault()}. If the Chromium UI is overridden the language
     * set will always be available.
     * @param isOverridden Boolean indicating if the Chromium UI is overridden.
     * @param isTopAndroidLanguageAvailable Boolean indicating if the top Android language can be
     *         the UI language.
     * @param isDefaultLanguageAvailable Boolean indicating if the default Android language can be
     *         the UI language.
     * @return The @UiAvailableTypes status of Android language settings.
     */
    @VisibleForTesting
    static @UiAvailableTypes int getUiAvailabilityStatus(
            boolean isOverridden,
            boolean isTopAndroidLanguageAvailable,
            boolean isDefaultLanguageAvailable) {
        if (isOverridden) {
            return UiAvailableTypes.OVERRIDDEN;
        }
        if (isTopAndroidLanguageAvailable) {
            return UiAvailableTypes.TOP_AVAILABLE;
        }
        if (isDefaultLanguageAvailable && !isTopAndroidLanguageAvailable) {
            return UiAvailableTypes.ONLY_DEFAULT_AVAILABLE;
        }
        return UiAvailableTypes.NONE_AVAILABLE;
    }

    /**
     * Return the correctness status of the current Chromium UI.  The Ui language is correct when it
     * matches the default Android system language. The native UI language can be different from the
     * Java UI language if Play Store daily hygiene has not run since setting an override language.
     * @param noLanguageAvailable Boolean indicating no Android system language is a Chromium
     *         language.
     * @param isJavaUiCorrect Boolean indicating if the Java UI language is correct.
     * @param isNativeUiCorrect Boolean indicating if the native UI language is correct.
     * @return The @UiCorrectnessTypes status of the UI.
     */
    @VisibleForTesting
    static @UiCorrectTypes int getUiCorrectnessStatus(
            boolean noLanguageAvailable, boolean isJavaUiCorrect, boolean isNativeUiCorrect) {
        if (noLanguageAvailable) {
            return UiCorrectTypes.NOT_AVAILABLE;
        }
        if (isJavaUiCorrect && isNativeUiCorrect) {
            return UiCorrectTypes.CORRECT;
        }
        if (isJavaUiCorrect && !isNativeUiCorrect) {
            return UiCorrectTypes.ONLY_JAVA_CORRECT;
        }
        return UiCorrectTypes.INCORRECT;
    }

    /**
     * Return the status of the current Chromium UI when the UI language is not overridden. The UI
     * language is correct when it matches the default Android system language. If no Android
     * language is available as the Chromium language then the UI is by definition incorrect.
     * @param isCorrect Boolean indicating the Chromium UI language matches the Android default.
     * @return The @UiCorrectnessTypes status of the UI.
     */
    @VisibleForTesting
    static @UiCorrectTypes int getNoOverrideUiCorrectStatus(
            boolean noLanguageAvailable, boolean isCorrect) {
        if (noLanguageAvailable) {
            return UiCorrectTypes.NOT_AVAILABLE;
        }
        if (isCorrect) {
            return UiCorrectTypes.CORRECT;
        }
        return UiCorrectTypes.INCORRECT;
    }

    /**
     * Return the status of the current Chromium UI when the UI language is overridden. The UI
     * language is correct when it matches the override UI value set as the default locale. The
     * native UI language can be different from the Java UI language if Play Store daily hygiene has
     * not run since setting an override language.
     * @param isJavaUiCorrect Boolean indicating if the Java UI language is correct.
     * @param isNativeUiCorrect Boolean indicating if the native UI language is correct.
     * @return The @UiCorrectnessTypes status of the UI.
     */
    @VisibleForTesting
    static @UiCorrectTypes int getOverrideUiCorrectStatus(
            boolean isJavaUiCorrect, boolean isNativeUiCorrect) {
        if (isJavaUiCorrect && isNativeUiCorrect) {
            return UiCorrectTypes.CORRECT;
        }
        if (isJavaUiCorrect && !isNativeUiCorrect) {
            return UiCorrectTypes.ONLY_JAVA_CORRECT;
        }
        return UiCorrectTypes.INCORRECT;
    }

    /**
     * Records the status of the previous system locale compared to the new system locale. The
     * histogram buckets are split based on if the Chrome app language is overridden.
     * @param previousLocale Comma separated string of the previous system locales.
     * @param currentLocale Comma separated string of the current system locales.
     */
    public static void recordLocaleUpdateStatus(String previousLocale, String currentLocale) {
        boolean isOverridden = GlobalAppLocaleController.getInstance().isOverridden();
        @LocaleUpdateStatus
        int status = getLocaleUpdateStatus(previousLocale, currentLocale, isOverridden);
        RecordHistogram.recordEnumeratedHistogram(
                "LanguageUsage.UI.Android.IsLocaleUpdated", status, LocaleUpdateStatus.NUM_ENTRIES);
    }

    /**
     * Returns the status of the current system locale compared to a previous locale. If the
     * previous locale is empty returns |LocaleUpdateStatus.DO_NOT_RECORD| since this is the first
     * run.
     * @param previousLocale Comma separated string of the previous system locales.
     * @param currentLocale Comma separated string of the current system locales.
     * @param isOverridden True if the Chrome app language is overridden.
     * @return The @LocaleUpdateStatus.
     */
    @VisibleForTesting
    static @LocaleUpdateStatus int getLocaleUpdateStatus(
            String previousLocale, String currentLocale, boolean isOverridden) {
        if (TextUtils.isEmpty(previousLocale) || TextUtils.isEmpty(currentLocale)) {
            return LocaleUpdateStatus.FIRST_RUN;
        }
        if (TextUtils.equals(previousLocale, currentLocale)) {
            return LocaleUpdateStatus.NO_CHANGE;
        }

        String[] previousLocaleList = previousLocale.split(",");
        String[] currentLocaleList = currentLocale.split(",");

        if (isOverridden) {
            if (TextUtils.equals(previousLocaleList[0], currentLocaleList[0])) {
                return LocaleUpdateStatus.OVERRIDDEN_OTHERS_CHANGED;
            }
            return LocaleUpdateStatus.OVERRIDDEN_TOP_CHANGED;
        }

        // Start no override
        if (TextUtils.equals(previousLocaleList[0], currentLocaleList[0])) {
            return LocaleUpdateStatus.NO_OVERRIDE_OTHERS_CHANGED;
        }
        return LocaleUpdateStatus.NO_OVERRIDE_TOP_CHANGED;
    }

    private ChromeLocalizationUtils() {
        /* cannot be instantiated */
    }
}