Merge "Add Bilingual Suggested locale adapter."

This commit is contained in:
Anna Zhuravleva
2022-06-17 12:55:58 +00:00
committed by Android (Google) Code Review
10 changed files with 379 additions and 24 deletions

View File

@@ -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<LocaleStore.LocaleInfo> 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);
}
}
}

View File

@@ -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;

View File

@@ -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)</p>
*/
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<LocaleStore.LocaleInfo> mLocaleOptions;
private ArrayList<LocaleStore.LocaleInfo> mOriginalLocaleOptions;
private int mSuggestionCount;
private final boolean mCountryMode;
private LayoutInflater mInflater;
protected ArrayList<LocaleStore.LocaleInfo> mLocaleOptions;
protected ArrayList<LocaleStore.LocaleInfo> 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<LocaleStore.LocaleInfo> 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.

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/frame"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:orientation="vertical">
<TextView
android:id="@+id/locale_native"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/language_picker_item_text_color"
android:textSize="18sp"
android:textAppearance="?android:attr/textAppearanceListItem"
/>
<TextView
android:id="@+id/locale_secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/language_picker_item_text_color_secondary"
android:textSize="16sp"
android:textAppearance="?android:attr/textAppearanceListItem"
/>
</LinearLayout>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2022 Google Inc.
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.
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
style="?android:attr/preferenceCategoryStyle"
android:layout_width="match_parent"
android:layout_height="36dp"
android:gravity="center_vertical"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:textColor="@color/language_picker_item_text_color"
android:textAllCaps="true"
tools:text="@string/language_picker_section_all"/>

View File

@@ -37,4 +37,8 @@
<color name="user_icon_6">#ff4ecde6</color><!-- cyan -->
<color name="user_icon_7">#fffbbc04</color><!-- yellow -->
<color name="user_icon_8">#fffa903e</color><!-- orange -->
<!-- Lily Language Picker language item view colors -->
<color name="language_picker_item_text_color">#F1F3F4</color>
<color name="language_picker_item_text_color_secondary">#BDC1C6</color>
</resources>

View File

@@ -450,4 +450,8 @@
<!-- Color of camera light when camera is in use -->
<color name="camera_privacy_light_day">#FFFFFF</color>
<color name="camera_privacy_light_night">#FFFFFF</color>
<!-- Lily Language Picker language item view colors -->
<color name="language_picker_item_text_color">#202124</color>
<color name="language_picker_item_text_color_secondary">#5F6368</color>
</resources>

View File

@@ -5421,6 +5421,11 @@
<!-- List section subheader for the language picker, containing a list of suggested languages determined by the default region [CHAR LIMIT=30] -->
<string name="language_picker_section_suggested">Suggested</string>
<!-- List section subheader for the language picker, containing a list of suggested languages determined by the default region [CHAR LIMIT=30] -->
<string name="language_picker_section_suggested_bilingual">Suggested languages</string>
<!-- List section subheader for the language picker, containing a list of suggested regions determined by the default region [CHAR LIMIT=30] -->
<string name="region_picker_section_suggested_bilingual">Suggested regions</string>
<!-- List section subheader for the language picker, containing a list of all languages available [CHAR LIMIT=30] -->
<string name="language_picker_section_all">All languages</string>
<!-- List section subheader for the region picker, containing a list of all regions supported for the selected language.

View File

@@ -108,6 +108,8 @@
<java-symbol type="id" name="list_item" />
<java-symbol type="id" name="listContainer" />
<java-symbol type="id" name="locale" />
<java-symbol type="id" name="locale_native" />
<java-symbol type="id" name="locale_secondary" />
<java-symbol type="id" name="matches" />
<java-symbol type="id" name="mediacontroller_progress" />
<java-symbol type="id" name="minute" />
@@ -3108,12 +3110,16 @@
<!-- Locale picker -->
<java-symbol type="id" name="locale_search_menu" />
<java-symbol type="layout" name="language_picker_item" />
<java-symbol type="layout" name="language_picker_bilingual_item" />
<java-symbol type="layout" name="language_picker_section_header" />
<java-symbol type="layout" name="language_picker_bilingual_section_header" />
<java-symbol type="menu" name="language_selection_list" />
<java-symbol type="string" name="country_selection_title" />
<java-symbol type="string" name="language_picker_section_all" />
<java-symbol type="string" name="region_picker_section_all" />
<java-symbol type="string" name="language_picker_section_suggested" />
<java-symbol type="string" name="language_picker_section_suggested_bilingual" />
<java-symbol type="string" name="region_picker_section_suggested_bilingual" />
<java-symbol type="string" name="language_selection_title" />
<java-symbol type="string" name="search_language_hint" />

View File

@@ -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<LocaleInfo> mLocaleOptions;
private HashSet<LocaleInfo> 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<Integer> getItemTypeList(final BilingualSuggestedLocaleAdapter adapter) {
return IntStream.range(0, adapter.getCount())
.mapToObj(adapter::getItemViewType)
.collect(Collectors.toList());
}
private static List<String> 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());
}
}