From 1b6f052e0fde2f00bc5fdfbf9538fb511d3eb626 Mon Sep 17 00:00:00 2001
From: Anna Zhuravleva
Date: Wed, 23 Feb 2022 18:00:31 +0000
Subject: [PATCH] Add Bilingual Suggested locale adapter.
Introduce Bilingual extension of base Suggested locale adapter and custom list item views.
Bug: 214045093
Test: atest, flashed to device
Change-Id: Iea8d7ff6271b0f66cb6a7e8e08f10aad957d7ec0
---
.../app/BilingualSuggestedLocaleAdapter.java | 130 +++++++++++++++++
.../com/android/internal/app/LocaleStore.java | 11 +-
.../internal/app/SuggestedLocaleAdapter.java | 54 +++++---
.../layout/language_picker_bilingual_item.xml | 31 +++++
...nguage_picker_bilingual_section_header.xml | 27 ++++
core/res/res/values-night/colors.xml | 4 +
core/res/res/values/colors.xml | 4 +
core/res/res/values/strings.xml | 5 +
core/res/res/values/symbols.xml | 6 +
.../BilingualSuggestedLocaleAdapterTest.java | 131 ++++++++++++++++++
10 files changed, 379 insertions(+), 24 deletions(-)
create mode 100644 core/java/com/android/internal/app/BilingualSuggestedLocaleAdapter.java
create mode 100644 core/res/res/layout/language_picker_bilingual_item.xml
create mode 100644 core/res/res/layout/language_picker_bilingual_section_header.xml
create mode 100644 core/tests/coretests/src/com/android/internal/app/BilingualSuggestedLocaleAdapterTest.java
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 9480362d4fe58..6520fc86acd97 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 c6b60f5860475..e3bb4ea9ecce8 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -5433,6 +5433,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());
+ }
+}