Merge changes from topic "app_selection_in_locale_picker" into tm-dev

* changes:
  Add subtitle in System language
  Reuse LocalePickerWithRegion in app language selection
This commit is contained in:
Calvin Pan
2022-03-22 03:35:28 +00:00
committed by Android (Google) Code Review
8 changed files with 293 additions and 13 deletions

View File

@@ -0,0 +1,85 @@
/*
* 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.app.LocaleConfig;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.LocaleList;
import android.util.Log;
import java.util.ArrayList;
import java.util.Locale;
public class AppLocaleStore {
private static final String TAG = AppLocaleStore.class.getSimpleName();
public static ArrayList<Locale> getAppSupportedLocales(Context context, String packageName) {
ArrayList<Locale> appSupportedLocales = new ArrayList<>();
LocaleList packageLocaleList = getPackageLocales(context, packageName);
if (packageLocaleList != null && packageLocaleList.size() > 0) {
for (int i = 0; i < packageLocaleList.size(); i++) {
appSupportedLocales.add(packageLocaleList.get(i));
}
Log.d(TAG, "getAppSupportedLocales from LocaleConfig. Size: "
+ appSupportedLocales.size());
} else {
String[] languages = getAssetLocales(context, packageName);
for (String language : languages) {
appSupportedLocales.add(Locale.forLanguageTag(language));
}
Log.d(TAG, "getAppSupportedLocales from asset. Size: "
+ appSupportedLocales.size());
}
return appSupportedLocales;
}
private static LocaleList getPackageLocales(Context context, String packageName) {
try {
LocaleConfig localeConfig =
new LocaleConfig(context.createPackageContext(packageName, 0));
if (localeConfig.getStatus() == LocaleConfig.STATUS_SUCCESS) {
return localeConfig.getSupportedLocales();
}
} catch (PackageManager.NameNotFoundException e) {
Log.w(TAG, "Can not found the package name : " + packageName + " / " + e);
}
return null;
}
private static String[] getAssetLocales(Context context, String packageName) {
try {
PackageManager packageManager = context.getPackageManager();
String[] locales = packageManager.getResourcesForApplication(
packageManager.getPackageInfo(packageName, PackageManager.MATCH_ALL)
.applicationInfo).getAssets().getNonSystemLocales();
if (locales == null) {
Log.i(TAG, "[" + packageName + "] locales are null.");
return new String[0];
} else if (locales.length <= 0) {
Log.i(TAG, "[" + packageName + "] locales length is 0.");
return new String[0];
}
return locales;
} catch (PackageManager.NameNotFoundException e) {
Log.w(TAG, "Can not found the package name : " + packageName + " / " + e);
}
return new String[0];
}
}

View File

@@ -234,7 +234,11 @@ public class LocaleHelper {
public int compare(LocaleStore.LocaleInfo lhs, LocaleStore.LocaleInfo rhs) {
// We don't care about the various suggestion types, just "suggested" (!= 0)
// and "all others" (== 0)
if (lhs.isSuggested() == rhs.isSuggested()) {
if (lhs.isAppCurrentLocale() || rhs.isAppCurrentLocale()) {
return lhs.isAppCurrentLocale() ? -1 : 1;
} else if (lhs.isSystemLocale() || rhs.isSystemLocale()) {
return lhs.isSystemLocale() ? -1 : 1;
} else if (lhs.isSuggested() == rhs.isSuggested()) {
// They are in the same "bucket" (suggested / others), so we compare the text
return mCollator.compare(
removePrefixForCompare(lhs.getLocale(), lhs.getLabel(mCountryMode)),

View File

@@ -23,6 +23,7 @@ import android.content.Context;
import android.os.Bundle;
import android.os.LocaleList;
import android.text.TextUtils;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@@ -32,6 +33,7 @@ import android.widget.SearchView;
import com.android.internal.R;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Locale;
@@ -45,6 +47,7 @@ import java.util.Set;
* default locale.</p>
*/
public class LocalePickerWithRegion extends ListFragment implements SearchView.OnQueryTextListener {
private static final String TAG = LocalePickerWithRegion.class.getSimpleName();
private static final String PARENT_FRAGMENT_NAME = "localeListEditor";
private SuggestedLocaleAdapter mAdapter;
@@ -57,6 +60,7 @@ public class LocalePickerWithRegion extends ListFragment implements SearchView.O
private boolean mPreviousSearchHadFocus = false;
private int mFirstVisiblePosition = 0;
private int mTopDistance = 0;
private String mAppPackageName;
/**
* Other classes can register to be notified when a locale was selected.
@@ -73,17 +77,25 @@ public class LocalePickerWithRegion extends ListFragment implements SearchView.O
private static LocalePickerWithRegion createCountryPicker(Context context,
LocaleSelectedListener listener, LocaleStore.LocaleInfo parent,
boolean translatedOnly) {
boolean translatedOnly, String appPackageName) {
LocalePickerWithRegion localePicker = new LocalePickerWithRegion();
boolean shouldShowTheList = localePicker.setListener(context, listener, parent,
translatedOnly);
translatedOnly, appPackageName);
return shouldShowTheList ? localePicker : null;
}
public static LocalePickerWithRegion createLanguagePicker(Context context,
LocaleSelectedListener listener, boolean translatedOnly) {
LocalePickerWithRegion localePicker = new LocalePickerWithRegion();
localePicker.setListener(context, listener, /* parent */ null, translatedOnly);
localePicker.setListener(context, listener, /* parent */ null, translatedOnly, null);
return localePicker;
}
public static LocalePickerWithRegion createLanguagePicker(Context context,
LocaleSelectedListener listener, boolean translatedOnly, String appPackageName) {
LocalePickerWithRegion localePicker = new LocalePickerWithRegion();
localePicker.setListener(
context, listener, /* parent */ null, translatedOnly, appPackageName);
return localePicker;
}
@@ -101,20 +113,32 @@ public class LocalePickerWithRegion extends ListFragment implements SearchView.O
* "pretending" it was selected, and return false.</p>
*/
private boolean setListener(Context context, LocaleSelectedListener listener,
LocaleStore.LocaleInfo parent, boolean translatedOnly) {
LocaleStore.LocaleInfo parent, boolean translatedOnly, String appPackageName) {
this.mParentLocale = parent;
this.mListener = listener;
this.mTranslatedOnly = translatedOnly;
this.mAppPackageName = appPackageName;
setRetainInstance(true);
final HashSet<String> langTagsToIgnore = new HashSet<>();
if (!translatedOnly) {
LocaleStore.LocaleInfo appCurrentLocale =
LocaleStore.getAppCurrentLocaleInfo(context, appPackageName);
boolean isForCountryMode = parent != null;
if (!TextUtils.isEmpty(appPackageName) && !isForCountryMode) {
if (appCurrentLocale != null) {
Log.d(TAG, "appCurrentLocale: " + appCurrentLocale.getLocale().toLanguageTag());
langTagsToIgnore.add(appCurrentLocale.getLocale().toLanguageTag());
} else {
Log.d(TAG, "appCurrentLocale is null");
}
} else if (!translatedOnly) {
final LocaleList userLocales = LocalePicker.getLocales();
final String[] langTags = userLocales.toLanguageTags().split(",");
Collections.addAll(langTagsToIgnore, langTags);
}
if (parent != null) {
if (isForCountryMode) {
mLocaleList = LocaleStore.getLevelLocales(context,
langTagsToIgnore, parent, translatedOnly);
if (mLocaleList.size() <= 1) {
@@ -127,10 +151,39 @@ public class LocalePickerWithRegion extends ListFragment implements SearchView.O
mLocaleList = LocaleStore.getLevelLocales(context, langTagsToIgnore,
null /* no parent */, translatedOnly);
}
Log.d(TAG, "mLocaleList size: " + mLocaleList.size());
// Adding current locale and system default option into suggestion list
if(!TextUtils.isEmpty(appPackageName)) {
if (appCurrentLocale != null && !isForCountryMode) {
mLocaleList.add(appCurrentLocale);
}
filterTheLanguagesNotSupportedInApp(context, appPackageName);
if (!isForCountryMode) {
mLocaleList.add(LocaleStore.getSystemDefaultLocaleInfo());
}
}
return true;
}
private void filterTheLanguagesNotSupportedInApp(Context context, String appPackageName) {
ArrayList<Locale> supportedLocales =
AppLocaleStore.getAppSupportedLocales(context, appPackageName);
Set<LocaleStore.LocaleInfo> filteredList = new HashSet<>();
for(LocaleStore.LocaleInfo li: mLocaleList) {
for(Locale l: supportedLocales) {
if(LocaleList.matchesLanguageAndScript(li.getLocale(), l)) {
filteredList.add(li);
}
}
}
Log.d(TAG, "mLocaleList after app-supported filter: " + filteredList.size());
mLocaleList = filteredList;
}
private void returnToParentFrame() {
getFragmentManager().popBackStack(PARENT_FRAGMENT_NAME,
FragmentManager.POP_BACK_STACK_INCLUSIVE);
@@ -151,7 +204,7 @@ public class LocalePickerWithRegion extends ListFragment implements SearchView.O
final boolean countryMode = mParentLocale != null;
final Locale sortingLocale = countryMode ? mParentLocale.getLocale() : Locale.getDefault();
mAdapter = new SuggestedLocaleAdapter(mLocaleList, countryMode);
mAdapter = new SuggestedLocaleAdapter(mLocaleList, countryMode, mAppPackageName);
final LocaleHelper.LocaleInfoComparator comp =
new LocaleHelper.LocaleInfoComparator(sortingLocale, countryMode);
mAdapter.sort(comp);
@@ -212,17 +265,22 @@ public class LocalePickerWithRegion extends ListFragment implements SearchView.O
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
SuggestedLocaleAdapter adapter = (SuggestedLocaleAdapter) getListAdapter();
final LocaleStore.LocaleInfo locale =
(LocaleStore.LocaleInfo) getListAdapter().getItem(position);
(LocaleStore.LocaleInfo) adapter.getItem(position);
// Special case for resetting the app locale to equal the system locale.
boolean isSystemLocale = locale.isSystemLocale();
boolean isRegionLocale = locale.getParent() != null;
if (locale.getParent() != null) {
if (isSystemLocale || isRegionLocale) {
if (mListener != null) {
mListener.onLocaleSelected(locale);
}
returnToParentFrame();
} else {
LocalePickerWithRegion selector = LocalePickerWithRegion.createCountryPicker(
getContext(), mListener, locale, mTranslatedOnly /* translate only */);
getContext(), mListener, locale, mTranslatedOnly /* translate only */,
adapter.getAppPackageName());
if (selector != null) {
getFragmentManager().beginTransaction()
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)

View File

@@ -16,11 +16,13 @@
package com.android.internal.app;
import android.app.LocaleManager;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.os.LocaleList;
import android.provider.Settings;
import android.telephony.TelephonyManager;
import android.util.Log;
import java.io.Serializable;
import java.util.HashMap;
@@ -31,12 +33,17 @@ import java.util.Set;
public class LocaleStore {
private static final HashMap<String, LocaleInfo> sLocaleCache = new HashMap<>();
private static final String TAG = LocaleStore.class.getSimpleName();
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;
// Only for per-app language picker
private static final int SUGGESTION_TYPE_CURRENT = 1 << 2;
// Only for per-app language picker
private static final int SUGGESTION_TYPE_SYSTEM_LANGUAGE = 1 << 3;
private final Locale mLocale;
private final Locale mParent;
@@ -189,6 +196,14 @@ public class LocaleStore {
public void setChecked(boolean checked) {
mIsChecked = checked;
}
public boolean isAppCurrentLocale() {
return (mSuggestionFlags & SUGGESTION_TYPE_CURRENT) > 0;
}
public boolean isSystemLocale() {
return (mSuggestionFlags & SUGGESTION_TYPE_SYSTEM_LANGUAGE) > 0;
}
}
private static Set<String> getSimCountries(Context context) {
@@ -239,6 +254,40 @@ public class LocaleStore {
}
}
public static LocaleInfo getAppCurrentLocaleInfo(Context context, String appPackageName) {
if (appPackageName == null) {
return null;
}
LocaleManager localeManager = context.getSystemService(LocaleManager.class);
try {
LocaleList localeList = (localeManager == null)
? null : localeManager.getApplicationLocales(appPackageName);
Locale locale = localeList == null ? null : localeList.get(0);
if (locale != null) {
LocaleInfo localeInfo = new LocaleInfo(locale);
localeInfo.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_CURRENT;
localeInfo.mIsTranslated = true;
return localeInfo;
}
} catch (IllegalArgumentException e) {
Log.d(TAG, "IllegalArgumentException ", e);
}
return null;
}
/**
* The "system default" is special case for per-app picker. Intentionally keep the locale
* empty to let activity know "system default" been selected.
*/
public static LocaleInfo getSystemDefaultLocaleInfo() {
LocaleInfo systemDefaultInfo = new LocaleInfo("");
systemDefaultInfo.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SYSTEM_LANGUAGE;
systemDefaultInfo.mIsTranslated = true;
return systemDefaultInfo;
}
/*
* Show all the languages supported for a country in the suggested list.
* This is also handy for devices without SIM (tablets).

View File

@@ -53,6 +53,7 @@ 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 MIN_REGIONS_FOR_SUGGESTIONS = 6;
private ArrayList<LocaleStore.LocaleInfo> mLocaleOptions;
@@ -64,10 +65,18 @@ public class SuggestedLocaleAdapter extends BaseAdapter implements Filterable {
private Locale mDisplayLocale = null;
// used to potentially cache a modified Context that uses mDisplayLocale
private Context mContextOverride = null;
private String mAppPackageName;
public SuggestedLocaleAdapter(Set<LocaleStore.LocaleInfo> localeOptions, boolean countryMode) {
this(localeOptions, countryMode, null);
}
public SuggestedLocaleAdapter(Set<LocaleStore.LocaleInfo> localeOptions, boolean countryMode,
String appPackageName) {
mCountryMode = countryMode;
mLocaleOptions = new ArrayList<>(localeOptions.size());
mAppPackageName = appPackageName;
for (LocaleStore.LocaleInfo li : localeOptions) {
if (li.isSuggested()) {
mSuggestionCount++;
@@ -83,7 +92,8 @@ public class SuggestedLocaleAdapter extends BaseAdapter implements Filterable {
@Override
public boolean isEnabled(int position) {
return getItemViewType(position) == TYPE_LOCALE;
return getItemViewType(position) == TYPE_LOCALE
|| getItemViewType(position) == TYPE_SYSTEM_LANGUAGE_FOR_APP_LANGUAGE_PICKER;
}
@Override
@@ -97,13 +107,20 @@ public class SuggestedLocaleAdapter extends BaseAdapter implements Filterable {
if (position == mSuggestionCount + 1) {
return TYPE_HEADER_ALL_OTHERS;
}
LocaleStore.LocaleInfo item = (LocaleStore.LocaleInfo) getItem(position);
if (item.isSystemLocale()) {
return TYPE_SYSTEM_LANGUAGE_FOR_APP_LANGUAGE_PICKER;
}
return TYPE_LOCALE;
}
}
@Override
public int getViewTypeCount() {
if (showHeaders()) {
if (!TextUtils.isEmpty(mAppPackageName) && showHeaders()) {
return 4; // Two headers and 1 for "System language"
} else if (showHeaders()) {
return 3; // Two headers in addition to the locales
} else {
return 1; // Locales items only
@@ -187,6 +204,21 @@ public class SuggestedLocaleAdapter extends BaseAdapter implements Filterable {
textView.setTextLocale(
mDisplayLocale != null ? mDisplayLocale : Locale.getDefault());
break;
case TYPE_SYSTEM_LANGUAGE_FOR_APP_LANGUAGE_PICKER:
if (!(convertView instanceof ViewGroup)) {
convertView = mInflater.inflate(
R.layout.app_language_picker_system_default, parent, false);
}
Locale defaultLocale = Locale.getDefault();
TextView title = convertView.findViewById(R.id.locale);
title.setText(R.string.system_locale_title);
title.setTextLocale(defaultLocale);
TextView subtitle = convertView.findViewById(R.id.system_locale_subtitle);
subtitle.setText(defaultLocale.getDisplayName());
subtitle.setTextLocale(defaultLocale);
break;
default:
// Covers both null, and "reusing" a wrong kind of view
if (!(convertView instanceof ViewGroup)) {
@@ -316,4 +348,8 @@ public class SuggestedLocaleAdapter extends BaseAdapter implements Filterable {
public Filter getFilter() {
return new FilterByNativeAndUiNames();
}
public String getAppPackageName() {
return mAppPackageName;
}
}

View File

@@ -0,0 +1,42 @@
<!--
~ 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.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minHeight="?android:attr/listPreferredItemHeight"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:orientation="vertical"
android:paddingTop="8dp">
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/locale"
android:gravity="center_vertical"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textDirection="locale"
android:layoutDirection="locale"
android:layout_weight="1" />
<TextView
android:id="@+id/system_locale_subtitle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
android:textColor="?android:attr/textColorSecondary"
android:layout_weight="1" />
</LinearLayout>

View File

@@ -6293,4 +6293,7 @@ ul.</string>
<!-- Strings for VirtualDeviceManager -->
<!-- Error message indicating the camera cannot be accessed when running on a virtual device. [CHAR LIMIT=NONE] -->
<string name="vdm_camera_access_denied">Cannot access camera from this device</string>
<!-- Title for preference of the system default locale. [CHAR LIMIT=50]-->
<string name="system_locale_title">System language</string>
</resources>

View File

@@ -4771,4 +4771,7 @@
<java-symbol type="drawable" name="ic_swap_horiz" />
<java-symbol type="bool" name="config_notificationForceUserSetOnUpgrade" />
<java-symbol type="string" name="system_locale_title" />
<java-symbol type="layout" name="app_language_picker_system_default" />
<java-symbol type="id" name="system_locale_subtitle" />
</resources>