blob: ce2f0d2a871a94100bd16b1d1bc2e03558cd9fb5 [file] [log] [blame]
// Copyright 2021 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.language;
import android.app.Activity;
import android.content.res.Resources;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.widget.ImageView;
import android.widget.RadioButton;
import android.widget.TextView;
import androidx.annotation.IntDef;
import androidx.annotation.VisibleForTesting;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import org.chromium.base.LocaleUtils;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.language.settings.LanguageItem;
import org.chromium.chrome.browser.language.settings.LanguagesManager;
import org.chromium.chrome.browser.translate.TranslateBridge;
import org.chromium.components.language.AndroidLanguageMetricsBridge;
import org.chromium.components.language.GeoLanguageProviderBridge;
import org.chromium.net.NetworkChangeNotifier;
import org.chromium.ui.modaldialog.DialogDismissalCause;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.modaldialog.ModalDialogProperties;
import org.chromium.ui.modaldialog.SimpleModalDialogController;
import org.chromium.ui.modelutil.PropertyModel;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Locale;
/**
* Implements a modal dialog that prompts the user to change their UI language. Displayed once at
* browser startup when no other promo or modals are shown.
*/
public class AppLanguagePromoDialog {
private Activity mActivity;
private ModalDialogManager mModalDialogManager;
private PropertyModel mAppLanguageModal;
private PropertyModel mLoadingModal;
private LanguageItemAdapter mAdapter;
private RestartAction mRestartAction;
/** Annotation for row item type. Either a LanguageItem or separator */
@IntDef({ItemType.LANGUAGE, ItemType.SEPARATOR, ItemType.MORE_LANGUAGES})
@Retention(RetentionPolicy.SOURCE)
private @interface ItemType {
int LANGUAGE = 0;
int SEPARATOR = 1;
int MORE_LANGUAGES = 2;
}
/**
* Annotation for the action taken on the language dialog.
* Do not reorder or remove items, only add new items before NUM_ENTRIES.
* Keep in sync with LanguageSettingsAppLanguagePromptAction from enums.xml.
*/
@IntDef({ActionType.DISMISSED_CANCEL_BUTTON, ActionType.DISMISSED_SYSTEM_BACK,
ActionType.OK_CHANGE_LANGUAGE, ActionType.OK_SAME_LANGUAGE, ActionType.OTHER})
@Retention(RetentionPolicy.SOURCE)
private @interface ActionType {
int DISMISSED_CANCEL_BUTTON = 0;
int DISMISSED_SYSTEM_BACK = 1;
int OK_CHANGE_LANGUAGE = 2;
int OK_SAME_LANGUAGE = 3;
int OTHER = 4;
int NUM_ENTRIES = 5;
}
/**
* Annotation for the TopULPMatch Histogram.
* Do not reorder or remove items, only add new items before NUM_ENTRIES.
* Keep in sync with ULPTopLanguageMatch from enums.xml.
*/
@IntDef({TopULPMatchType.NO, TopULPMatchType.YES, TopULPMatchType.EMPTY,
TopULPMatchType.NUM_ENTRIES})
@Retention(RetentionPolicy.SOURCE)
public @interface TopULPMatchType {
int NO = 0;
int YES = 1;
int EMPTY = 2;
int NUM_ENTRIES = 3;
}
/**
* Interface for holding the Chrome restart action.
*/
@FunctionalInterface
public interface RestartAction {
void restart();
}
public AppLanguagePromoDialog(Activity activity,
ObservableSupplier<ModalDialogManager> modalDialogManagerSupplier,
RestartAction restartAction) {
mActivity = activity;
mModalDialogManager = modalDialogManagerSupplier.get();
mRestartAction = restartAction;
Resources resources = mActivity.getResources();
mAppLanguageModal =
new PropertyModel.Builder(ModalDialogProperties.ALL_KEYS)
.with(ModalDialogProperties.CONTROLLER,
new SimpleModalDialogController(
mModalDialogManager, this::onDismissAppLanguageModal))
.with(ModalDialogProperties.TITLE, resources, R.string.languages_srp_title)
.with(ModalDialogProperties.POSITIVE_BUTTON_TEXT, resources, R.string.ok)
.with(ModalDialogProperties.NEGATIVE_BUTTON_TEXT, resources,
R.string.cancel)
.with(ModalDialogProperties.BUTTON_STYLES,
ModalDialogProperties.ButtonStyles.PRIMARY_FILLED_NEGATIVE_OUTLINE)
.build();
mLoadingModal = new PropertyModel.Builder(ModalDialogProperties.ALL_KEYS)
.with(ModalDialogProperties.CONTROLLER,
new SimpleModalDialogController(
mModalDialogManager, this::onDismissConfirmModal))
.build();
}
/**
* Internal class for managing a list of languages in a RecyclerView.
* TODO(https://crbug.com/1325473) Refactor this to a separate file.
*/
protected static class LanguageItemAdapter
extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private ArrayList<LanguageItem> mTopLanguages;
private ArrayList<LanguageItem> mOtherLanguages;
private LanguageItem mCurrentLanguage;
private boolean mShowOtherLanguages;
/**
* @param topLanguages - LanguageItems to appear at the top of the adapter list.
* @param otherLanguages - LanguageItems to appear below the top languages.
* @param currentLanguage - The currently selected app language.
*/
public LanguageItemAdapter(Collection<LanguageItem> topLanguages,
Collection<LanguageItem> otherLanguages, LanguageItem currentLanguage) {
mTopLanguages = new ArrayList<LanguageItem>(topLanguages);
mOtherLanguages = new ArrayList<LanguageItem>(otherLanguages);
mCurrentLanguage = currentLanguage;
}
@Override
public int getItemViewType(int position) {
// The separator or "More languages" item is between top and other languages.
if (position != mTopLanguages.size()) return ItemType.LANGUAGE;
return mShowOtherLanguages ? ItemType.SEPARATOR : ItemType.MORE_LANGUAGES;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
case ItemType.LANGUAGE:
return new AppLanguagePromptRowViewHolder(
LayoutInflater.from(parent.getContext())
.inflate(R.layout.app_language_prompt_row, parent, false));
case ItemType.MORE_LANGUAGES:
return new MoreLanguagesRowViewHolder(
LayoutInflater.from(parent.getContext())
.inflate(R.layout.app_language_prompt_more_languages, parent,
false));
case ItemType.SEPARATOR:
return new SeparatorViewHolder(
LayoutInflater.from(parent.getContext())
.inflate(R.layout.language_ask_prompt_row_separator, parent,
false));
default:
assert false : "No matching viewType";
return null;
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (getItemViewType(position) == ItemType.LANGUAGE) {
LanguageItem languageItem = getLanguageItemAt(position);
((AppLanguagePromptRowViewHolder) holder)
.bindViewHolder(languageItem, languageItem.equals(mCurrentLanguage));
}
}
/**
* Modify the LanguageItemAdapter to show the other languages in addition to the top
* languages. Can only called once. The other languages can not be hidden once shown.
*/
public void showOtherLanguages() {
mShowOtherLanguages = true;
notifyItemRemoved(mTopLanguages.size()); // Remove "More languages" item.
// Other languages plus a horizontal separator have been added.
notifyItemRangeInserted(mTopLanguages.size(), mOtherLanguages.size() + 1);
}
/**
* Set the currently selected LanguageItem based on the position.
* TODO(https://crbug.com/1325522) Refactor to not use notifyDataSetChanged.
* @param position Offset of the LanguageItem to select.
*/
public void setSelectedLanguage(int position) {
mCurrentLanguage = getLanguageItemAt(position);
notifyDataSetChanged();
}
/**
* Return the number of items in the list making room for the list separator or more
* languages item.
*/
@Override
public int getItemCount() {
// The top languages and a separator or "More languages" item are always shown.
int count = mTopLanguages.size() + 1;
if (mShowOtherLanguages) {
count += mOtherLanguages.size();
}
return count;
}
public LanguageItem getSelectedLanguage() {
return mCurrentLanguage;
}
public boolean isTopLanguageSelected() {
return mTopLanguages.contains(mCurrentLanguage);
}
public boolean areOtherLanguagesShown() {
return mShowOtherLanguages;
}
protected LanguageItem getLanguageItemAt(int position) {
if (position < mTopLanguages.size()) {
return mTopLanguages.get(position);
} else if (position > mTopLanguages.size()) {
// Other languages are offset by one from the seperator.
return mOtherLanguages.get(position - mTopLanguages.size() - 1);
}
assert false : "The language item at the separator can not be accessed";
return null;
}
}
/**
* Internal class representing an individual language row.
*/
private static class AppLanguagePromptRowViewHolder
extends RecyclerView.ViewHolder implements View.OnClickListener {
private TextView mPrimaryNameTextView;
private TextView mSecondaryNameTextView;
private RadioButton mRadioButton;
AppLanguagePromptRowViewHolder(View view) {
super(view);
mPrimaryNameTextView = ((TextView) itemView.findViewById(R.id.primary_language_name));
mSecondaryNameTextView =
((TextView) itemView.findViewById(R.id.secondary_language_name));
mRadioButton =
((RadioButton) itemView.findViewById(R.id.app_language_prompt_radiobutton));
view.setOnClickListener(this);
mRadioButton.setOnClickListener(this);
}
@Override
public void onClick(View row) {
LanguageItemAdapter adapter = (LanguageItemAdapter) getBindingAdapter();
int position = getBindingAdapterPosition();
if (position == RecyclerView.NO_POSITION) {
// NO_POSITION will be returned if no layout pass has been done since the last call
// to RecyclerView.Adapter.notifyDataSetChanged(). In that case we should not
// update the new selected language.
return;
}
adapter.setSelectedLanguage(position);
View positiveButton = row.getRootView().findViewById(R.id.positive_button);
if (positiveButton != null) {
positiveButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
}
}
public void bindViewHolder(LanguageItem languageItem, boolean checked) {
mRadioButton.setChecked(checked);
mRadioButton.setContentDescription(languageItem.getDisplayName());
if (languageItem.isSystemDefault()) {
// For the system default locale the display name should be the primary TextView.
mPrimaryNameTextView.setText(languageItem.getDisplayName());
mSecondaryNameTextView.setText(languageItem.getNativeDisplayName());
} else {
mPrimaryNameTextView.setText(languageItem.getNativeDisplayName());
mSecondaryNameTextView.setText(languageItem.getDisplayName());
}
}
}
/**
* Internal class representing the "More languages" list item.
*/
private static class MoreLanguagesRowViewHolder
extends RecyclerView.ViewHolder implements View.OnClickListener {
MoreLanguagesRowViewHolder(View view) {
super(view);
view.setOnClickListener(this);
}
@Override
public void onClick(View row) {
// TODO(https://crbug.com/1325471) Add meteric recording action.
LanguageItemAdapter adapter = (LanguageItemAdapter) getBindingAdapter();
adapter.showOtherLanguages();
}
}
/**
* Internal class representing the separator row.
*/
private static class SeparatorViewHolder extends RecyclerView.ViewHolder {
SeparatorViewHolder(View view) {
super(view);
}
}
/**
* Show the app language modal and add a custom view holding a list of languages with the
* current location's and users preferred languages at the top.
*/
protected void showAppLanguageModal() {
// Setup initial language lists.
LanguageItem currentOverrideLanguage =
LanguagesManager.getInstance().getLanguageItem(AppLocaleUtils.getAppLanguagePref());
LinkedHashSet<LanguageItem> uiLanguages = new LinkedHashSet<LanguageItem>(
LanguagesManager.getInstance().getAllPossibleUiLanguages());
LinkedHashSet<LanguageItem> topLanguages =
getTopLanguages(uiLanguages, currentOverrideLanguage);
uiLanguages.removeAll(topLanguages);
mAdapter = new LanguageItemAdapter(topLanguages, uiLanguages, currentOverrideLanguage);
// Release all static LanguagesManager resources since they are no longer needed.
LanguagesManager.recycle();
// Setup RecyclerView.
View customView = LayoutInflater.from(mActivity).inflate(
R.layout.app_language_prompt_content, null, false);
RecyclerView list = customView.findViewById(R.id.app_language_prompt_content_recycler_view);
list.setAdapter(mAdapter);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(mActivity);
linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
list.setLayoutManager(linearLayoutManager);
// Make top and bottom shadow visible when needed.
ImageView topShadow = customView.findViewById(R.id.top_shadow);
ImageView bottomShadow = customView.findViewById(R.id.bottom_shadow);
list.setOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (recyclerView.canScrollVertically(-1)) {
topShadow.setVisibility(View.VISIBLE);
} else {
topShadow.setVisibility(View.GONE);
}
if (recyclerView.canScrollVertically(1)) {
bottomShadow.setVisibility(View.VISIBLE);
} else {
bottomShadow.setVisibility(View.GONE);
}
}
});
mAppLanguageModal.set(ModalDialogProperties.CUSTOM_VIEW, customView);
mModalDialogManager.showDialog(mAppLanguageModal, ModalDialogManager.ModalDialogType.APP);
}
public void onDismissAppLanguageModal(@DialogDismissalCause int dismissalCause) {
if (dismissalCause == DialogDismissalCause.POSITIVE_BUTTON_CLICKED) {
String languageCode = mAdapter.getSelectedLanguage().getCode();
if (AppLocaleUtils.isAppLanguagePref(languageCode)) {
recordDismissAction(ActionType.OK_SAME_LANGUAGE);
} else {
recordDismissAction(ActionType.OK_CHANGE_LANGUAGE);
}
startAppLanguageInstall();
} else if (dismissalCause == DialogDismissalCause.NEGATIVE_BUTTON_CLICKED) {
recordDismissAction(ActionType.DISMISSED_CANCEL_BUTTON);
} else if (dismissalCause == DialogDismissalCause.NAVIGATE_BACK_OR_TOUCH_OUTSIDE) {
recordDismissAction(ActionType.DISMISSED_SYSTEM_BACK);
} else {
recordDismissAction(ActionType.OTHER);
}
recordOtherLanguagesShown(mAdapter.areOtherLanguagesShown());
TranslateBridge.setAppLanguagePromptShown();
}
public void onDismissConfirmModal(@DialogDismissalCause int dismissalCause) {
// No action is taken if the confirm modal is dismissed.
}
/**
* Return an ordered set of LanguageItems that should be shown at the top of the list. These
* languages come from the user's current location and preferred languages. The original
* system language is replaced with a value that follows the current device language and is
* added to the top of the list.
* @param uiLanguages Collection of possible UI languages.
* @param currentOverrideLanguage The LanguageItem representing the current UI language.
* @return An ordered set of LanguageItems.
*/
private static LinkedHashSet<LanguageItem> getTopLanguages(
Collection<LanguageItem> uiLanguages, LanguageItem currentOverrideLanguage) {
LinkedHashSet<String> topLanguageCodes =
new LinkedHashSet<>(GeoLanguageProviderBridge.getCurrentGeoLanguages());
topLanguageCodes.addAll(TranslateBridge.getUserLanguageCodes());
Locale originalSystemLocale =
GlobalAppLocaleController.getInstance().getOriginalSystemLocale();
return getTopLanguagesHelper(
uiLanguages, topLanguageCodes, currentOverrideLanguage, originalSystemLocale);
}
/**
* Helper function isolating the logic for making the top language list for testing. Adds the
* system default language to the top of the list if needed and only adds top languages that are
* possible UI languages.
* @param uiLanguages Collection of possible UI languages.
* @param topLanguageCodes Ordered set of potential top languages tags.
* @param currentOverrideLanguage The LanguageItem representing the current UI language.
* @param originalSystemLocale Locale of the original device language before any override.
* @return An ordered set of LanguageItems.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
static LinkedHashSet<LanguageItem> getTopLanguagesHelper(Collection<LanguageItem> uiLanguages,
LinkedHashSet<String> topLanguageCodes, LanguageItem currentOverrideLanguage,
Locale originalSystemLocale) {
// Remove the exact language from top language codes if there are multiple UI languages
// with the same base, otherwise remove the base language.
if (AppLocaleUtils.hasMultipleUiLanguageVariants(originalSystemLocale.toLanguageTag())) {
topLanguageCodes.remove(originalSystemLocale.toLanguageTag());
} else {
topLanguageCodes.remove(originalSystemLocale.getLanguage());
}
// The system default language should always be at the top of the list unless the current
// override language is equal to the original system language. In that case only the
// current override language is added to the top of the list.
LinkedHashSet<LanguageItem> topLanguages = new LinkedHashSet<>();
if (currentOverrideLanguage.isSystemDefault()) {
topLanguages.add(LanguageItem.makeFollowSystemLanguageItem());
} else if (!isOverrideLanguageOriginalSystemLanguage(
currentOverrideLanguage, originalSystemLocale)) {
topLanguages.add(LanguageItem.makeFollowSystemLanguageItem());
topLanguages.add(currentOverrideLanguage);
} else {
// The current override language can only be the original system language if it has
// already been changed in settings. The option to track the system language is not
// given in the app language promo - but can be reset from Language Settings.
topLanguages.add(currentOverrideLanguage);
}
// Make a map of code -> LanguageItem for UI languages
HashMap<String, LanguageItem> uiLanguagesMap = new HashMap<>();
for (LanguageItem item : uiLanguages) {
uiLanguagesMap.put(item.getCode(), item);
}
// Only add top languages that can be UI languages.
for (String code : topLanguageCodes) {
LanguageItem item = uiLanguagesMap.get(code);
if (item != null) topLanguages.add(item);
}
return topLanguages;
}
/**
* Returns true if the current override language is the same as the original system language.
* For languages that have only one Chrome UI language variant the base languages are compared
* and for languages with multiple Chrome UI languages the full language tag is compared.
* @param overrideLanguage LanguageItem for the current override language.
* @param originalSystemLocale String language code for the original system locale.
* @return Whether or not the override language is the same as the original system language.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
static boolean isOverrideLanguageOriginalSystemLanguage(
LanguageItem overrideLanguage, Locale originalSystemLocale) {
if (overrideLanguage.isSystemDefault()) {
return false;
}
if (AppLocaleUtils.hasMultipleUiLanguageVariants(overrideLanguage.getCode())) {
return TextUtils.equals(
overrideLanguage.getCode(), originalSystemLocale.toLanguageTag());
}
return LocaleUtils.isBaseLanguageEqual(
overrideLanguage.getCode(), originalSystemLocale.toLanguageTag());
}
/**
* Show the confirm modal with a progress spinner and start the language split install. Chrome
* will restart once the language split has been downloaded. If the download fails an error
* message is shown.
*/
private void startAppLanguageInstall() {
View customView = LayoutInflater.from(mActivity).inflate(
R.layout.app_language_confirm_content, null, false);
LanguageItem selectedLanguage = mAdapter.getSelectedLanguage();
CharSequence messageText = mActivity.getResources().getString(
R.string.languages_srp_loading_text, selectedLanguage.getDisplayName());
TextView messageView = customView.findViewById(R.id.message);
messageView.setText(messageText);
mLoadingModal.set(ModalDialogProperties.CUSTOM_VIEW, customView);
// Only show the modal if the selected language is not installed.
if (!LanguageSplitInstaller.getInstance().isLanguageSplitInstalled(
selectedLanguage.getCode())) {
mModalDialogManager.showDialog(mLoadingModal, ModalDialogManager.ModalDialogType.APP);
}
boolean isSelectedCurrentUI = AppLocaleUtils.isAppLanguagePref(selectedLanguage.getCode());
if (!isSelectedCurrentUI) {
// Only record isTopLanguage if the app language has changed.
recordIsTopLanguage(mAdapter.isTopLanguageSelected());
}
AndroidLanguageMetricsBridge.reportAppLanguagePromptLanguage(
selectedLanguage.isSystemDefault() ? "" : selectedLanguage.getCode());
// Create call back for after language split install completes.
AppLocaleUtils.setAppLanguagePref(selectedLanguage.getCode(), (success) -> {
if (success) {
if (!isSelectedCurrentUI) {
// Only restart if the new language is different than the current UI.
mRestartAction.restart();
}
} else {
// The loading language modal will always already be shown if a download fails.
CharSequence failedText = mActivity.getResources().getString(
R.string.languages_split_failed, selectedLanguage.getDisplayName());
messageView.setText(failedText);
mLoadingModal.set(ModalDialogProperties.POSITIVE_BUTTON_TEXT,
mActivity.getText(R.string.ok).toString());
}
});
}
/**
* Displays starts showing the App language prompt if the experiment is enabled.
* @param activity The current activity to display the prompt into.
* @param modalDialogManagerSupplier Supplier of {@link ModalDialogManager}.
* @return Whether the prompt was shown or not.
*/
public static boolean maybeShowPrompt(Activity activity,
ObservableSupplier<ModalDialogManager> modalDialogManagerSupplier,
RestartAction restartAction) {
if (!shouldShowPrompt()) return false;
AppLanguagePromoDialog prompt =
new AppLanguagePromoDialog(activity, modalDialogManagerSupplier, restartAction);
prompt.showAppLanguageModal();
return true;
}
/**
* @return Whether the app language prompt should be shown or not.
*/
private static boolean shouldShowPrompt() {
// Skip feature and preference checks if forced on for testing.
if (!ChromeFeatureList.isEnabled(ChromeFeatureList.FORCE_APP_LANGUAGE_PROMPT)) {
// Don't show if prompt has already been shown.
if (TranslateBridge.getAppLanguagePromptShown()) return false;
@TopULPMatchType
int hasULPMatch =
LanguageBridge.isTopULPBaseLanguage(Locale.getDefault().toLanguageTag());
recordTopULPMatchStatus(hasULPMatch);
// Don't show if not enabled.
if (!ChromeFeatureList.isEnabled(ChromeFeatureList.APP_LANGUAGE_PROMPT)) return false;
// Don't show if ULP match is enabled and the UI language doesn't match the top ULP
// language.
if (ChromeFeatureList.isEnabled(ChromeFeatureList.APP_LANGUAGE_PROMPT_ULP)
&& hasULPMatch != TopULPMatchType.NO) {
return false;
}
}
boolean isOnline = NetworkChangeNotifier.isOnline();
recordOnlineStatus(isOnline);
// Only show the prompt if online.
return isOnline;
}
/**
* Record the action type when dismissing the dialog and how long the dialog was shown for.
* @param @ActionType int.
*/
private static void recordDismissAction(@ActionType int actionType) {
RecordHistogram.recordEnumeratedHistogram(
"LanguageSettings.AppLanguagePrompt.Action", actionType, ActionType.NUM_ENTRIES);
}
private static void recordOnlineStatus(boolean isOnline) {
RecordHistogram.recordBooleanHistogram(
"LanguageSettings.AppLanguagePrompt.IsOnline", isOnline);
}
private static void recordTopULPMatchStatus(@TopULPMatchType int hasMatch) {
RecordHistogram.recordEnumeratedHistogram(
"LanguageSettings.AppLanguagePrompt.TopULPMatchStatus", hasMatch,
TopULPMatchType.NUM_ENTRIES);
}
private static void recordIsTopLanguage(boolean isTopLanguage) {
RecordHistogram.recordBooleanHistogram(
"LanguageSettings.AppLanguagePrompt.IsTopLanguageSelected", isTopLanguage);
}
private static void recordOtherLanguagesShown(boolean shown) {
RecordHistogram.recordBooleanHistogram(
"LanguageSettings.AppLanguagePrompt.OtherLanguagesShown", shown);
}
}