Reuse LocalePickerWithRegion in app language selection
- (LocalePickerWithRegion) Only list the language that supported by app
- Add "System language" option
- Add current app locale into suggestion list
- Sort locales by these priorities:
1. Current app's locale
2. System language
3. Suggested language
4. Supported languages
Bug: 223090003
Test: Manual
- (system languages page)
Check Settings-> System -> Languages -> Languages
- (pre-apps language page)
Check Settings-> System -> Languages -> Apps Languages
- Add unit test in follow CL(tracing in b/224747202)
Change-Id: I8dc9f73811dbf653b32773fb98ddb2e4656a4432
This commit is contained in:
85
core/java/com/android/internal/app/AppLocaleStore.java
Normal file
85
core/java/com/android/internal/app/AppLocaleStore.java
Normal 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];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -21,6 +21,7 @@ import android.annotation.Nullable;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -64,10 +65,14 @@ 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) {
|
||||
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++;
|
||||
@@ -195,11 +200,20 @@ public class SuggestedLocaleAdapter extends BaseAdapter implements Filterable {
|
||||
|
||||
TextView text = (TextView) convertView.findViewById(R.id.locale);
|
||||
LocaleStore.LocaleInfo item = (LocaleStore.LocaleInfo) getItem(position);
|
||||
text.setText(item.getLabel(mCountryMode));
|
||||
text.setTextLocale(item.getLocale());
|
||||
text.setContentDescription(item.getContentDescription(mCountryMode));
|
||||
Locale layoutLocale;
|
||||
if (item.isSystemLocale()) {
|
||||
// show system default option
|
||||
text.setText(R.string.system_locale_title);
|
||||
text.setTextLocale(Locale.getDefault());
|
||||
layoutLocale = Locale.getDefault();
|
||||
} else {
|
||||
text.setText(item.getLabel(mCountryMode));
|
||||
text.setTextLocale(item.getLocale());
|
||||
text.setContentDescription(item.getContentDescription(mCountryMode));
|
||||
layoutLocale = item.getParent();
|
||||
}
|
||||
if (mCountryMode) {
|
||||
int layoutDir = TextUtils.getLayoutDirectionFromLocale(item.getParent());
|
||||
int layoutDir = TextUtils.getLayoutDirectionFromLocale(layoutLocale);
|
||||
//noinspection ResourceType
|
||||
convertView.setLayoutDirection(layoutDir);
|
||||
text.setTextDirection(layoutDir == View.LAYOUT_DIRECTION_RTL
|
||||
@@ -316,4 +330,8 @@ public class SuggestedLocaleAdapter extends BaseAdapter implements Filterable {
|
||||
public Filter getFilter() {
|
||||
return new FilterByNativeAndUiNames();
|
||||
}
|
||||
|
||||
public String getAppPackageName() {
|
||||
return mAppPackageName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6296,4 +6296,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>
|
||||
|
||||
@@ -4772,4 +4772,5 @@
|
||||
<java-symbol type="drawable" name="ic_swap_horiz" />
|
||||
<java-symbol type="bool" name="config_notificationForceUserSetOnUpgrade" />
|
||||
|
||||
<java-symbol type="string" name="system_locale_title" />
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user