diff --git a/core/java/com/android/internal/app/AppLocaleStore.java b/core/java/com/android/internal/app/AppLocaleStore.java new file mode 100644 index 0000000000000..76e58988eedf4 --- /dev/null +++ b/core/java/com/android/internal/app/AppLocaleStore.java @@ -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 getAppSupportedLocales(Context context, String packageName) { + ArrayList 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]; + } + +} diff --git a/core/java/com/android/internal/app/LocaleHelper.java b/core/java/com/android/internal/app/LocaleHelper.java index 707286e0fc4ca..57bd3f9453582 100644 --- a/core/java/com/android/internal/app/LocaleHelper.java +++ b/core/java/com/android/internal/app/LocaleHelper.java @@ -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)), diff --git a/core/java/com/android/internal/app/LocalePickerWithRegion.java b/core/java/com/android/internal/app/LocalePickerWithRegion.java index b4ae56f234433..e5b33f9abd83c 100644 --- a/core/java/com/android/internal/app/LocalePickerWithRegion.java +++ b/core/java/com/android/internal/app/LocalePickerWithRegion.java @@ -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.

*/ 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.

*/ 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 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 supportedLocales = + AppLocaleStore.getAppSupportedLocales(context, appPackageName); + + Set 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) diff --git a/core/java/com/android/internal/app/LocaleStore.java b/core/java/com/android/internal/app/LocaleStore.java index 1c5ca593ebbae..cea8eaa3ee7fa 100644 --- a/core/java/com/android/internal/app/LocaleStore.java +++ b/core/java/com/android/internal/app/LocaleStore.java @@ -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 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 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). diff --git a/core/java/com/android/internal/app/SuggestedLocaleAdapter.java b/core/java/com/android/internal/app/SuggestedLocaleAdapter.java index 46f47a31441c0..10855f488940f 100644 --- a/core/java/com/android/internal/app/SuggestedLocaleAdapter.java +++ b/core/java/com/android/internal/app/SuggestedLocaleAdapter.java @@ -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 localeOptions, boolean countryMode) { + public SuggestedLocaleAdapter(Set 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; + } } diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 04a70cb67d8cb..8c17870822697 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -6296,4 +6296,7 @@ ul. Cannot access camera from this device + + + System language diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 1f0b22b9e4603..4bb2aa590a2a5 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -4772,4 +4772,5 @@ +