diff --git a/core/java/com/android/internal/app/BilingualSuggestedLocaleAdapter.java b/core/java/com/android/internal/app/BilingualSuggestedLocaleAdapter.java new file mode 100644 index 0000000000000..d2cb30e2363e9 --- /dev/null +++ b/core/java/com/android/internal/app/BilingualSuggestedLocaleAdapter.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.app; + +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.android.internal.R; + +import java.util.Locale; +import java.util.Set; + +/** + * This adapter extends basic SuggestedLocaleAdapter. In addition to the base functionality, it + * shows language name not only in the native locale, but in secondary locale as well. Secondary + * locale is passed as a constructor parameter. + */ +public class BilingualSuggestedLocaleAdapter extends SuggestedLocaleAdapter { + + private final Locale mSecondaryLocale; + private final int mSecondaryLocaleTextDir; + + public BilingualSuggestedLocaleAdapter( + Set localeOptions, + boolean countryMode, + Locale secondaryLocale) { + super(localeOptions, countryMode); + mSecondaryLocale = secondaryLocale; + if (TextUtils.getLayoutDirectionFromLocale(secondaryLocale) == View.LAYOUT_DIRECTION_RTL) { + mSecondaryLocaleTextDir = View.TEXT_DIRECTION_RTL; + } else { + mSecondaryLocaleTextDir = View.TEXT_DIRECTION_LTR; + } + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null && super.mInflater == null) { + mInflater = LayoutInflater.from(parent.getContext()); + } + + int itemType = getItemViewType(position); + switch (itemType) { + case TYPE_HEADER_SUGGESTED: // intentional fallthrough + case TYPE_HEADER_ALL_OTHERS: + // Covers both null, and "reusing" a wrong kind of view + if (!(convertView instanceof TextView)) { + convertView = + mInflater.inflate( + R.layout.language_picker_bilingual_section_header, + parent, + false); + } + TextView textView = (TextView) convertView; + if (itemType == TYPE_HEADER_SUGGESTED) { + setHeaderText( + textView, + R.string.language_picker_section_suggested_bilingual, + R.string.region_picker_section_suggested_bilingual); + } else { + setHeaderText( + textView, + R.string.language_picker_section_all, + R.string.region_picker_section_all); + } + break; + default: + // Covers both null, and "reusing" a wrong kind of view + if (!(convertView instanceof ViewGroup)) { + convertView = + mInflater.inflate( + R.layout.language_picker_bilingual_item, parent, false); + } + + LocaleStore.LocaleInfo item = (LocaleStore.LocaleInfo) getItem(position); + setLocaleToListItem(convertView, item); + } + return convertView; + } + + private void setHeaderText( + TextView textView, int languageStringResourceId, int regionStringResourceId) { + if (mCountryMode) { + setTextTo(textView, regionStringResourceId); + } else { + setTextTo(textView, languageStringResourceId); + } + } + + private void setLocaleToListItem(View itemView, LocaleStore.LocaleInfo localeInfo) { + if (localeInfo == null) { + throw new NullPointerException("Cannot set locale, locale info is null."); + } + + TextView textNative = (TextView) itemView.findViewById(R.id.locale_native); + textNative.setText(localeInfo.getLabel(mCountryMode)); + textNative.setTextLocale(localeInfo.getLocale()); + textNative.setContentDescription(localeInfo.getContentDescription(mCountryMode)); + + TextView textSecondary = (TextView) itemView.findViewById(R.id.locale_secondary); + textSecondary.setText(localeInfo.getLocale().getDisplayLanguage(mSecondaryLocale)); + textSecondary.setTextDirection(mSecondaryLocaleTextDir); + if (mCountryMode) { + int layoutDir = TextUtils.getLayoutDirectionFromLocale(localeInfo.getParent()); + //noinspection ResourceType + itemView.setLayoutDirection(layoutDir); + textNative.setTextDirection( + layoutDir == View.LAYOUT_DIRECTION_RTL + ? View.TEXT_DIRECTION_RTL + : View.TEXT_DIRECTION_LTR); + } + } +} diff --git a/core/java/com/android/internal/app/LocaleStore.java b/core/java/com/android/internal/app/LocaleStore.java index 1ad5e89410fa1..689dec479c896 100644 --- a/core/java/com/android/internal/app/LocaleStore.java +++ b/core/java/com/android/internal/app/LocaleStore.java @@ -24,6 +24,8 @@ import android.provider.Settings; import android.telephony.TelephonyManager; import android.util.Log; +import com.android.internal.annotations.VisibleForTesting; + import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; @@ -39,9 +41,9 @@ public class LocaleStore { private static boolean sFullyInitialized = false; public static class LocaleInfo implements Serializable { - private static final int SUGGESTION_TYPE_NONE = 0; - private static final int SUGGESTION_TYPE_SIM = 1 << 0; - private static final int SUGGESTION_TYPE_CFG = 1 << 1; + @VisibleForTesting static final int SUGGESTION_TYPE_NONE = 0; + @VisibleForTesting static final int SUGGESTION_TYPE_SIM = 1 << 0; + @VisibleForTesting static final int SUGGESTION_TYPE_CFG = 1 << 1; // Only for per-app language picker private static final int SUGGESTION_TYPE_CURRENT = 1 << 2; // Only for per-app language picker @@ -55,7 +57,8 @@ public class LocaleStore { private boolean mIsChecked; // Used by the LocaleListEditor to mark entries for deletion // Combination of flags for various reasons to show a locale as a suggestion. // Can be SIM, location, etc. - private int mSuggestionFlags; + // Set to public to be accessible during runtime from the test app. + @VisibleForTesting public int mSuggestionFlags; private String mFullNameNative; private String mFullCountryNameNative; diff --git a/core/java/com/android/internal/app/SuggestedLocaleAdapter.java b/core/java/com/android/internal/app/SuggestedLocaleAdapter.java index ff188dc6f2b9b..fcdcb2dadb54e 100644 --- a/core/java/com/android/internal/app/SuggestedLocaleAdapter.java +++ b/core/java/com/android/internal/app/SuggestedLocaleAdapter.java @@ -37,7 +37,6 @@ import java.util.Collections; import java.util.Locale; import java.util.Set; - /** * This adapter wraps around a regular ListAdapter for LocaleInfo, and creates 2 sections. * @@ -51,25 +50,25 @@ import java.util.Set; * (Austria, Belgium, Germany, Liechtenstein, Luxembourg)

*/ public class SuggestedLocaleAdapter extends BaseAdapter implements Filterable { - private static final int TYPE_HEADER_SUGGESTED = 0; - private static final int TYPE_HEADER_ALL_OTHERS = 1; - private static final int TYPE_LOCALE = 2; - private static final int TYPE_SYSTEM_LANGUAGE_FOR_APP_LANGUAGE_PICKER = 3; - private static final int TYPE_CURRENT_LOCALE = 4; - private static final int MIN_REGIONS_FOR_SUGGESTIONS = 6; - private static final int APP_LANGUAGE_PICKER_TYPE_COUNT = 5; - private static final int SYSTEM_LANGUAGE_TYPE_COUNT = 3; - private static final int SYSTEM_LANGUAGE_WITHOUT_HEADER_TYPE_COUNT = 1; + protected static final int TYPE_HEADER_SUGGESTED = 0; + protected static final int TYPE_HEADER_ALL_OTHERS = 1; + protected static final int TYPE_LOCALE = 2; + protected static final int TYPE_SYSTEM_LANGUAGE_FOR_APP_LANGUAGE_PICKER = 3; + protected static final int TYPE_CURRENT_LOCALE = 4; + protected static final int MIN_REGIONS_FOR_SUGGESTIONS = 6; + protected static final int APP_LANGUAGE_PICKER_TYPE_COUNT = 5; + protected static final int SYSTEM_LANGUAGE_TYPE_COUNT = 3; + protected static final int SYSTEM_LANGUAGE_WITHOUT_HEADER_TYPE_COUNT = 1; - private ArrayList mLocaleOptions; - private ArrayList mOriginalLocaleOptions; - private int mSuggestionCount; - private final boolean mCountryMode; - private LayoutInflater mInflater; + protected ArrayList mLocaleOptions; + protected ArrayList mOriginalLocaleOptions; + protected int mSuggestionCount; + protected final boolean mCountryMode; + protected LayoutInflater mInflater; - private Locale mDisplayLocale = null; + protected Locale mDisplayLocale = null; // used to potentially cache a modified Context that uses mDisplayLocale - private Context mContextOverride = null; + protected Context mContextOverride = null; private String mAppPackageName; public SuggestedLocaleAdapter(Set localeOptions, boolean countryMode) { @@ -122,6 +121,9 @@ public class SuggestedLocaleAdapter extends BaseAdapter implements Filterable { } LocaleStore.LocaleInfo item = (LocaleStore.LocaleInfo) getItem(position); + if (item == null) { + throw new NullPointerException("Non header locale cannot be null"); + } if (item.isSystemLocale()) { return TYPE_SYSTEM_LANGUAGE_FOR_APP_LANGUAGE_PICKER; } @@ -156,6 +158,10 @@ public class SuggestedLocaleAdapter extends BaseAdapter implements Filterable { @Override public Object getItem(int position) { + if (isHeaderPosition(position)) { + return null; + } + int offset = 0; if (showHeaders()) { offset = position > mSuggestionCount ? -2 : -1; @@ -164,6 +170,10 @@ public class SuggestedLocaleAdapter extends BaseAdapter implements Filterable { return mLocaleOptions.get(position + offset); } + private boolean isHeaderPosition(int position) { + return showHeaders() && (position == 0 || position == mSuggestionCount + 1); + } + @Override public long getItemId(int position) { return position; @@ -185,7 +195,7 @@ public class SuggestedLocaleAdapter extends BaseAdapter implements Filterable { } } - private void setTextTo(@NonNull TextView textView, int resId) { + protected void setTextTo(@NonNull TextView textView, int resId) { if (mContextOverride == null) { textView.setText(resId); } else { @@ -220,7 +230,11 @@ public class SuggestedLocaleAdapter extends BaseAdapter implements Filterable { break; case TYPE_SYSTEM_LANGUAGE_FOR_APP_LANGUAGE_PICKER: TextView title; - if (((LocaleStore.LocaleInfo)getItem(position)).isAppCurrentLocale()) { + LocaleStore.LocaleInfo info = (LocaleStore.LocaleInfo) getItem(position); + if (info == null) { + throw new NullPointerException("Non header locale cannot be null."); + } + if (info.isAppCurrentLocale()) { title = itemView.findViewById(R.id.language_picker_item); } else { title = itemView.findViewById(R.id.locale); @@ -292,7 +306,7 @@ public class SuggestedLocaleAdapter extends BaseAdapter implements Filterable { return updatedView; } - private boolean showHeaders() { + protected boolean showHeaders() { // We don't want to show suggestions for locales with very few regions // (e.g. Romanian, with 2 regions) // So we put a (somewhat) arbitrary limit. diff --git a/core/res/res/layout/language_picker_bilingual_item.xml b/core/res/res/layout/language_picker_bilingual_item.xml new file mode 100644 index 0000000000000..f56dda9fd1dbe --- /dev/null +++ b/core/res/res/layout/language_picker_bilingual_item.xml @@ -0,0 +1,31 @@ + + + + + + + + \ No newline at end of file diff --git a/core/res/res/layout/language_picker_bilingual_section_header.xml b/core/res/res/layout/language_picker_bilingual_section_header.xml new file mode 100644 index 0000000000000..2dbf739963d5b --- /dev/null +++ b/core/res/res/layout/language_picker_bilingual_section_header.xml @@ -0,0 +1,27 @@ + + + + diff --git a/core/res/res/values-night/colors.xml b/core/res/res/values-night/colors.xml index 33c9b95c5819d..e2db49cfa657f 100644 --- a/core/res/res/values-night/colors.xml +++ b/core/res/res/values-night/colors.xml @@ -37,4 +37,8 @@ #ff4ecde6 #fffbbc04 #fffa903e + + + #F1F3F4 + #BDC1C6 diff --git a/core/res/res/values/colors.xml b/core/res/res/values/colors.xml index b515abc4000fa..faf48f3c3fdc0 100644 --- a/core/res/res/values/colors.xml +++ b/core/res/res/values/colors.xml @@ -450,4 +450,8 @@ #FFFFFF #FFFFFF + + + #202124 + #5F6368 diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index cf850de61a902..5ddfce0846ed8 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -5421,6 +5421,11 @@ Suggested + + + Suggested languages + + Suggested regions All languages + + + + diff --git a/core/tests/coretests/src/com/android/internal/app/BilingualSuggestedLocaleAdapterTest.java b/core/tests/coretests/src/com/android/internal/app/BilingualSuggestedLocaleAdapterTest.java new file mode 100644 index 0000000000000..e8aac113c312a --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/app/BilingualSuggestedLocaleAdapterTest.java @@ -0,0 +1,131 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.app; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.app.LocaleHelper.LocaleInfoComparator; +import com.android.internal.app.LocaleStore.LocaleInfo; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +@RunWith(AndroidJUnit4.class) +public class BilingualSuggestedLocaleAdapterTest { + + private HashSet mLocaleOptions; + private HashSet mEnglishCountryOptions; + + @Before + public void setUp() { + mLocaleOptions = new HashSet<>(); + mLocaleOptions.add(LocaleStore.getLocaleInfo(Locale.US)); + mLocaleOptions.add(LocaleStore.getLocaleInfo(Locale.GERMANY)); + mLocaleOptions.add(LocaleStore.getLocaleInfo(Locale.JAPAN)); + LocaleInfo korea = LocaleStore.getLocaleInfo(Locale.KOREA); + korea.setTranslated(true); + korea.mSuggestionFlags = LocaleInfo.SUGGESTION_TYPE_SIM; + mLocaleOptions.add(korea); + + mEnglishCountryOptions = new HashSet<>(); + mEnglishCountryOptions.add(LocaleStore.getLocaleInfo(Locale.US)); + mEnglishCountryOptions.add(LocaleStore.getLocaleInfo(Locale.UK)); + mEnglishCountryOptions.add(LocaleStore.getLocaleInfo(Locale.CANADA)); + mEnglishCountryOptions.add(LocaleStore.getLocaleInfo(Locale.forLanguageTag("en-IN"))); + mEnglishCountryOptions.add(LocaleStore.getLocaleInfo(Locale.forLanguageTag("en-HK"))); + mEnglishCountryOptions.add(LocaleStore.getLocaleInfo(Locale.forLanguageTag("en-SG"))); + LocaleInfo australianEnglish = LocaleStore.getLocaleInfo(Locale.forLanguageTag("en-AU")); + australianEnglish.setTranslated(true); + australianEnglish.mSuggestionFlags = LocaleInfo.SUGGESTION_TYPE_SIM; + mEnglishCountryOptions.add(australianEnglish); + } + + @Test + public void suggestedLocaleAdapter_notCountryMode_shouldDisplayLocalesSorted() { + BilingualSuggestedLocaleAdapter suggestedLocaleAdapter = + new BilingualSuggestedLocaleAdapter( + mLocaleOptions, /* countryMode */ false, Locale.US); + suggestedLocaleAdapter.sort(new LocaleInfoComparator(Locale.US, /* countryMode */ false)); + + assertThat(getItemTypeList(suggestedLocaleAdapter)) + .containsExactly( + SuggestedLocaleAdapter.TYPE_HEADER_SUGGESTED, + SuggestedLocaleAdapter.TYPE_LOCALE, + SuggestedLocaleAdapter.TYPE_HEADER_ALL_OTHERS, + SuggestedLocaleAdapter.TYPE_LOCALE, + SuggestedLocaleAdapter.TYPE_LOCALE, + SuggestedLocaleAdapter.TYPE_LOCALE) + .inOrder(); + + assertThat(getLocaleTagList(suggestedLocaleAdapter)) + .containsExactly(null, "ko-KR", null, "de-DE", "en-US", "ja-JP") + .inOrder(); + } + + @Test + public void suggestedLocaleAdapter_countryMode_shouldDisplayCountriesSorted() { + BilingualSuggestedLocaleAdapter suggestedLocaleAdapter = + new BilingualSuggestedLocaleAdapter( + mEnglishCountryOptions, /* countryMode */ true, Locale.US); + suggestedLocaleAdapter.sort(new LocaleInfoComparator(Locale.US, /* countryMode */ true)); + + assertThat(getItemTypeList(suggestedLocaleAdapter)) + .containsExactly( + SuggestedLocaleAdapter.TYPE_HEADER_SUGGESTED, + SuggestedLocaleAdapter.TYPE_LOCALE, + SuggestedLocaleAdapter.TYPE_HEADER_ALL_OTHERS, + SuggestedLocaleAdapter.TYPE_LOCALE, + SuggestedLocaleAdapter.TYPE_LOCALE, + SuggestedLocaleAdapter.TYPE_LOCALE, + SuggestedLocaleAdapter.TYPE_LOCALE, + SuggestedLocaleAdapter.TYPE_LOCALE, + SuggestedLocaleAdapter.TYPE_LOCALE) + .inOrder(); + assertThat(getLocaleTagList(suggestedLocaleAdapter)) + .containsExactly( + null, "en-AU", null, "en-CA", "en-HK", "en-IN", "en-SG", "en-GB", "en-US") + .inOrder(); + } + + private static List getItemTypeList(final BilingualSuggestedLocaleAdapter adapter) { + return IntStream.range(0, adapter.getCount()) + .mapToObj(adapter::getItemViewType) + .collect(Collectors.toList()); + } + + private static List getLocaleTagList(final BilingualSuggestedLocaleAdapter adapter) { + return IntStream.range(0, adapter.getCount()) + .mapToObj( + position -> { + LocaleInfo localeInfo = (LocaleInfo) adapter.getItem(position); + if (localeInfo == null) { + return null; + } + return localeInfo.getLocale().toLanguageTag(); + }) + .collect(Collectors.toList()); + } +}