Merge "Move locale negotiation to ResourcesManager"
diff --git a/api/current.txt b/api/current.txt
index fc69955..e35458a 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -10108,7 +10108,6 @@
method public java.lang.String getQuantityString(int, int, java.lang.Object...) throws android.content.res.Resources.NotFoundException;
method public java.lang.String getQuantityString(int, int) throws android.content.res.Resources.NotFoundException;
method public java.lang.CharSequence getQuantityText(int, int) throws android.content.res.Resources.NotFoundException;
- method public java.util.Locale getResolvedLocale();
method public java.lang.String getResourceEntryName(int) throws android.content.res.Resources.NotFoundException;
method public java.lang.String getResourceName(int) throws android.content.res.Resources.NotFoundException;
method public java.lang.String getResourcePackageName(int) throws android.content.res.Resources.NotFoundException;
diff --git a/api/system-current.txt b/api/system-current.txt
index 0fe4934..0f7d9aa 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -10504,7 +10504,6 @@
method public java.lang.String getQuantityString(int, int, java.lang.Object...) throws android.content.res.Resources.NotFoundException;
method public java.lang.String getQuantityString(int, int) throws android.content.res.Resources.NotFoundException;
method public java.lang.CharSequence getQuantityText(int, int) throws android.content.res.Resources.NotFoundException;
- method public java.util.Locale getResolvedLocale();
method public java.lang.String getResourceEntryName(int) throws android.content.res.Resources.NotFoundException;
method public java.lang.String getResourceName(int) throws android.content.res.Resources.NotFoundException;
method public java.lang.String getResourcePackageName(int) throws android.content.res.Resources.NotFoundException;
diff --git a/api/test-current.txt b/api/test-current.txt
index 3348468..c6269fc 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -10116,7 +10116,6 @@
method public java.lang.String getQuantityString(int, int, java.lang.Object...) throws android.content.res.Resources.NotFoundException;
method public java.lang.String getQuantityString(int, int) throws android.content.res.Resources.NotFoundException;
method public java.lang.CharSequence getQuantityText(int, int) throws android.content.res.Resources.NotFoundException;
- method public java.util.Locale getResolvedLocale();
method public java.lang.String getResourceEntryName(int) throws android.content.res.Resources.NotFoundException;
method public java.lang.String getResourceName(int) throws android.content.res.Resources.NotFoundException;
method public java.lang.String getResourcePackageName(int) throws android.content.res.Resources.NotFoundException;
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index dedc8e5..1e7457c 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -4916,8 +4916,10 @@
/*
* Initialize the default locales in this process for the reasons we set the time zone.
+ *
+ * We do this through ResourcesManager, since we need to do locale negotiation.
*/
- LocaleList.setDefault(data.config.getLocales());
+ mResourcesManager.setDefaultLocalesLocked(data.config.getLocales());
/*
* Update the system configuration since its preloaded and might not
diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java
index 260216c..94e584e 100644
--- a/core/java/android/app/ResourcesManager.java
+++ b/core/java/android/app/ResourcesManager.java
@@ -35,6 +35,9 @@
import android.view.DisplayAdjustments;
import java.lang.ref.WeakReference;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
/** @hide */
public class ResourcesManager {
@@ -42,11 +45,15 @@
private static final boolean DEBUG = false;
private static ResourcesManager sResourcesManager;
- private final ArrayMap<ResourcesKey, WeakReference<Resources> > mActiveResources =
+ private final ArrayMap<ResourcesKey, WeakReference<Resources>> mActiveResources =
new ArrayMap<>();
private final ArrayMap<Pair<Integer, DisplayAdjustments>, WeakReference<Display>> mDisplays =
new ArrayMap<>();
+ private String[] mSystemLocales = {};
+ private final HashSet<String> mNonSystemLocales = new HashSet<String>();
+ private boolean mHasNonSystemLocales = false;
+
CompatibilityInfo mResCompatibilityInfo;
Configuration mResConfiguration;
@@ -165,6 +172,8 @@
? new Configuration(overrideConfiguration) : null;
ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfigCopy, scale);
Resources r;
+ final boolean findSystemLocales;
+ final boolean hasNonSystemLocales;
synchronized (this) {
// Resources is app scale dependent.
if (DEBUG) Slog.w(TAG, "getTopLevelResources: " + resDir + " / " + scale);
@@ -178,6 +187,8 @@
+ " key=" + key + " overrideConfig=" + overrideConfiguration);
return r;
}
+ findSystemLocales = (mSystemLocales.length == 0);
+ hasNonSystemLocales = mHasNonSystemLocales;
}
//if (r != null) {
@@ -243,6 +254,18 @@
if (DEBUG) Slog.i(TAG, "Created app resources " + resDir + " " + r + ": "
+ r.getConfiguration() + " appScale=" + r.getCompatibilityInfo().applicationScale);
+ final String[] systemLocales = (
+ findSystemLocales ?
+ AssetManager.getSystem().getLocales() :
+ null);
+ final String[] nonSystemLocales = assets.getNonSystemLocales();
+ // Avoid checking for non-pseudo-locales if we already know there were some from a previous
+ // Resources. The default value (for when hasNonSystemLocales is true) doesn't matter,
+ // since mHasNonSystemLocales will also be true, and thus isPseudoLocalesOnly would not be
+ // able to affect mHasNonSystemLocales.
+ final boolean isPseudoLocalesOnly = hasNonSystemLocales ||
+ LocaleList.isPseudoLocalesOnly(nonSystemLocales);
+
synchronized (this) {
WeakReference<Resources> wr = mActiveResources.get(key);
Resources existing = wr != null ? wr.get() : null;
@@ -255,11 +278,30 @@
// XXX need to remove entries when weak references go away
mActiveResources.put(key, new WeakReference<>(r));
+ if (mSystemLocales.length == 0) {
+ mSystemLocales = systemLocales;
+ }
+ mNonSystemLocales.addAll(Arrays.asList(nonSystemLocales));
+ mHasNonSystemLocales = mHasNonSystemLocales || !isPseudoLocalesOnly;
if (DEBUG) Slog.v(TAG, "mActiveResources.size()=" + mActiveResources.size());
return r;
}
}
+ /* package */ void setDefaultLocalesLocked(LocaleList locales) {
+ final int bestLocale;
+ if (mHasNonSystemLocales) {
+ bestLocale = locales.getFirstMatchIndexWithEnglishSupported(mNonSystemLocales);
+ } else {
+ // We fallback to system locales if there was no locale specifically supported by the
+ // assets. This is to properly support apps that only rely on the shared system assets
+ // and don't need assets of their own.
+ bestLocale = locales.getFirstMatchIndexWithEnglishSupported(mSystemLocales);
+ }
+ // set it for Java, this also affects newly created Resources
+ LocaleList.setDefault(locales, bestLocale);
+ }
+
final boolean applyConfigurationToResourcesLocked(Configuration config,
CompatibilityInfo compat) {
if (mResConfiguration == null) {
@@ -283,13 +325,28 @@
| ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE;
}
- // set it for java, this also affects newly created Resources
- final LocaleList localeList = config.getLocales();
- if (!localeList.isEmpty()) {
- LocaleList.setDefault(localeList);
+ Configuration localeAdjustedConfig = config;
+ final LocaleList configLocales = config.getLocales();
+ if (!configLocales.isEmpty()) {
+ setDefaultLocalesLocked(configLocales);
+ final LocaleList adjustedLocales = LocaleList.getAdjustedDefault();
+ if (adjustedLocales != configLocales) { // has the same result as .equals() in this case
+ // The first locale in the list was not chosen. So we create a modified
+ // configuration with the adjusted locales (which moves the chosen locale to the
+ // front).
+ localeAdjustedConfig = new Configuration();
+ localeAdjustedConfig.setTo(config);
+ localeAdjustedConfig.setLocales(adjustedLocales);
+ // Also adjust the locale list in mResConfiguration, so that the Resources created
+ // later would have the same locale list.
+ if (!mResConfiguration.getLocales().equals(adjustedLocales)) {
+ mResConfiguration.setLocales(adjustedLocales);
+ changes |= ActivityInfo.CONFIG_LOCALE;
+ }
+ }
}
- Resources.updateSystemConfiguration(config, defaultDisplayMetrics, compat);
+ Resources.updateSystemConfiguration(localeAdjustedConfig, defaultDisplayMetrics, compat);
ApplicationPackageManager.configurationChanged();
//Slog.i(TAG, "Configuration changed in " + currentPackageName());
@@ -301,7 +358,7 @@
Resources r = mActiveResources.valueAt(i).get();
if (r != null) {
if (DEBUG || DEBUG_CONFIGURATION) Slog.v(TAG, "Changing resources "
- + r + " config to: " + config);
+ + r + " config to: " + localeAdjustedConfig);
int displayId = key.mDisplayId;
boolean isDefaultDisplay = (displayId == Display.DEFAULT_DISPLAY);
DisplayMetrics dm = defaultDisplayMetrics;
@@ -310,7 +367,7 @@
if (tmpConfig == null) {
tmpConfig = new Configuration();
}
- tmpConfig.setTo(config);
+ tmpConfig.setTo(localeAdjustedConfig);
if (!isDefaultDisplay) {
dm = getDisplayMetricsLocked(displayId);
applyNonDefaultDisplayMetricsToConfigurationLocked(dm, tmpConfig);
@@ -320,7 +377,7 @@
}
r.updateConfiguration(tmpConfig, dm, compat);
} else {
- r.updateConfiguration(config, dm, compat);
+ r.updateConfiguration(localeAdjustedConfig, dm, compat);
}
//Slog.i(TAG, "Updated app resources " + v.getKey()
// + " " + r + ": " + r.getConfiguration());
diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java
index ed64ead..e404429 100644
--- a/core/java/android/content/res/Resources.java
+++ b/core/java/android/content/res/Resources.java
@@ -162,10 +162,6 @@
private final Configuration mConfiguration = new Configuration();
- // Invariant: mResolvedLocale is the resolved locale of mLocalesForResolved
- private LocaleList mLocalesForResolved = null;
- private Locale mResolvedLocale = null;
-
private PluralRules mPluralRule;
private CompatibilityInfo mCompatibilityInfo = CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO;
@@ -321,16 +317,6 @@
}
/**
- * Return the Locale resulting from locale negotiation between the Resources and the
- * Configuration objects used to construct the Resources. The locale is used for retrieving
- * resources as well as for determining plural rules.
- */
- @NonNull
- public Locale getResolvedLocale() {
- return mResolvedLocale;
- }
-
- /**
* Return the string value associated with a particular resource ID. The
* returned object will be a String if this is a plain string; it will be
* some other type of CharSequence if it is styled.
@@ -394,7 +380,7 @@
private PluralRules getPluralRule() {
synchronized (sSync) {
if (mPluralRule == null) {
- mPluralRule = PluralRules.forLocale(mResolvedLocale);
+ mPluralRule = PluralRules.forLocale(mConfiguration.getLocales().getPrimary());
}
return mPluralRule;
}
@@ -457,7 +443,7 @@
@NonNull
public String getString(@StringRes int id, Object... formatArgs) throws NotFoundException {
final String raw = getString(id);
- return String.format(mResolvedLocale, raw, formatArgs);
+ return String.format(mConfiguration.getLocales().getPrimary(), raw, formatArgs);
}
/**
@@ -488,7 +474,7 @@
public String getQuantityString(@PluralsRes int id, int quantity, Object... formatArgs)
throws NotFoundException {
String raw = getQuantityText(id, quantity).toString();
- return String.format(mResolvedLocale, raw, formatArgs);
+ return String.format(mConfiguration.getLocales().getPrimary(), raw, formatArgs);
}
/**
@@ -1955,7 +1941,7 @@
LocaleList locales = mConfiguration.getLocales();
if (locales.isEmpty()) {
- locales = LocaleList.getDefault();
+ locales = LocaleList.getAdjustedDefault();
mConfiguration.setLocales(locales);
}
if (mConfiguration.densityDpi != Configuration.DENSITY_DPI_UNDEFINED) {
@@ -1983,26 +1969,8 @@
keyboardHidden = mConfiguration.keyboardHidden;
}
- if (locales != mLocalesForResolved) {
- if (locales.size() == 1) {
- // This is an optimization to avoid the JNI call(s) when the result of
- // getFirstMatchWithEnglishSupported() does not depend on the supported locales.
- mResolvedLocale = locales.getPrimary();
- } else {
- String[] supportedLocales = mAssets.getNonSystemLocales();
- if (LocaleList.isPseudoLocalesOnly(supportedLocales)) {
- // We fallback to all locales (including system locales) if there was no
- // locale specifically supported by the assets. This is to properly support
- // apps that only rely on the shared system assets and don't need assets of
- // their own.
- supportedLocales = mAssets.getLocales();
- }
- mResolvedLocale = locales.getFirstMatchWithEnglishSupported(supportedLocales);
- }
- mLocalesForResolved = locales;
- }
mAssets.setConfiguration(mConfiguration.mcc, mConfiguration.mnc,
- adjustLanguageTag(mResolvedLocale.toLanguageTag()),
+ adjustLanguageTag(locales.getPrimary().toLanguageTag()),
mConfiguration.orientation,
mConfiguration.touchscreen,
mConfiguration.densityDpi, mConfiguration.keyboard,
@@ -2027,7 +1995,7 @@
}
synchronized (sSync) {
if (mPluralRule != null) {
- mPluralRule = PluralRules.forLocale(mResolvedLocale);
+ mPluralRule = PluralRules.forLocale(mConfiguration.getLocales().getPrimary());
}
}
}
diff --git a/core/java/android/util/LocaleList.java b/core/java/android/util/LocaleList.java
index 8a2d015..90a20bc 100644
--- a/core/java/android/util/LocaleList.java
+++ b/core/java/android/util/LocaleList.java
@@ -26,6 +26,8 @@
import com.android.internal.annotations.GuardedBy;
+import java.util.Arrays;
+import java.util.Collection;
import java.util.HashSet;
import java.util.Locale;
@@ -317,55 +319,72 @@
return supportedScr.equals(desiredScr) ? 1 : 0;
}
+ private int findFirstMatchIndex(Locale supportedLocale) {
+ for (int idx = 0; idx < mList.length; idx++) {
+ final int score = matchScore(supportedLocale, mList[idx]);
+ if (score > 0) {
+ return idx;
+ }
+ }
+ return Integer.MAX_VALUE;
+ }
+
private static final Locale EN_LATN = Locale.forLanguageTag("en-Latn");
- private Locale computeFirstMatch(String[] supportedLocales, boolean assumeEnglishIsSupported) {
+ private int computeFirstMatchIndex(Collection<String> supportedLocales,
+ boolean assumeEnglishIsSupported) {
if (mList.length == 1) { // just one locale, perhaps the most common scenario
- return mList[0];
+ return 0;
}
if (mList.length == 0) { // empty locale list
- return null;
+ return -1;
}
+
int bestIndex = Integer.MAX_VALUE;
- final int numSupportedLocales =
- supportedLocales.length + (assumeEnglishIsSupported ? 1 : 0);
- for (int i = 0; i < numSupportedLocales; i++) {
- final Locale supportedLocale;
- if (assumeEnglishIsSupported) {
- // Try English first, so we can return early if it's in the LocaleList
- supportedLocale = (i == 0) ? EN_LATN : Locale.forLanguageTag(supportedLocales[i-1]);
- } else {
- supportedLocale = Locale.forLanguageTag(supportedLocales[i]);
+ // Try English first, so we can return early if it's in the LocaleList
+ if (assumeEnglishIsSupported) {
+ final int idx = findFirstMatchIndex(EN_LATN);
+ if (idx == 0) { // We have a match on the first locale, which is good enough
+ return 0;
+ } else if (idx < bestIndex) {
+ bestIndex = idx;
}
+ }
+ for (String languageTag : supportedLocales) {
+ final Locale supportedLocale = Locale.forLanguageTag(languageTag);
// We expect the average length of locale lists used for locale resolution to be
// smaller than three, so it's OK to do this as an O(mn) algorithm.
- for (int idx = 0; idx < mList.length; idx++) {
- final int score = matchScore(supportedLocale, mList[idx]);
- if (score > 0) {
- if (idx == 0) { // We have a match on the first locale, which is good enough
- return mList[0];
- } else if (idx < bestIndex) {
- bestIndex = idx;
- }
- }
+ final int idx = findFirstMatchIndex(supportedLocale);
+ if (idx == 0) { // We have a match on the first locale, which is good enough
+ return 0;
+ } else if (idx < bestIndex) {
+ bestIndex = idx;
}
}
- if (bestIndex == Integer.MAX_VALUE) { // no match was found
- return mList[0];
+ if (bestIndex == Integer.MAX_VALUE) {
+ // no match was found, so we fall back to the first locale in the locale list
+ return 0;
} else {
- return mList[bestIndex];
+ return bestIndex;
}
}
+ private Locale computeFirstMatch(Collection<String> supportedLocales,
+ boolean assumeEnglishIsSupported) {
+ int bestIndex = computeFirstMatchIndex(supportedLocales, assumeEnglishIsSupported);
+ return bestIndex == -1 ? null : mList[bestIndex];
+ }
+
/**
* Returns the first match in the locale list given an unordered array of supported locales
- * in BCP47 format.
+ * in BCP 47 format.
*
* If the locale list is empty, null would be returned.
*/
@Nullable
public Locale getFirstMatch(String[] supportedLocales) {
- return computeFirstMatch(supportedLocales, false /* assume English is not supported */);
+ return computeFirstMatch(Arrays.asList(supportedLocales),
+ false /* assume English is not supported */);
}
/**
@@ -374,11 +393,26 @@
*/
@Nullable
public Locale getFirstMatchWithEnglishSupported(String[] supportedLocales) {
- return computeFirstMatch(supportedLocales, true /* assume English is supported */);
+ return computeFirstMatch(Arrays.asList(supportedLocales),
+ true /* assume English is supported */);
}
/**
- * Returns true if the array of locale tags only contains empty locales and pseudolocales.
+ * {@hide}
+ */
+ public int getFirstMatchIndexWithEnglishSupported(Collection<String> supportedLocales) {
+ return computeFirstMatchIndex(supportedLocales, true /* assume English is supported */);
+ }
+
+ /**
+ * {@hide}
+ */
+ public int getFirstMatchIndexWithEnglishSupported(String[] supportedLocales) {
+ return getFirstMatchIndexWithEnglishSupported(Arrays.asList(supportedLocales));
+ }
+
+ /**
+ * Returns true if the collection of locale tags only contains empty locales and pseudolocales.
* Assumes that there is no repetition in the input.
* {@hide}
*/
@@ -386,7 +420,7 @@
if (supportedLocales.length > NUM_PSEUDO_LOCALES + 1) {
// This is for optimization. Since there's no repetition in the input, if we have more
// than the number of pseudo-locales plus one for the empty string, it's guaranteed
- // that we have some meaninful locale in the list, so the list is not "practically
+ // that we have some meaninful locale in the collection, so the list is not "practically
// empty".
return false;
}
@@ -405,6 +439,8 @@
@GuardedBy("sLock")
private static LocaleList sDefaultLocaleList = null;
@GuardedBy("sLock")
+ private static LocaleList sDefaultAdjustedLocaleList = null;
+ @GuardedBy("sLock")
private static Locale sLastDefaultLocale = null;
/**
@@ -415,8 +451,8 @@
* secondary preference is.
*
* Note that the default LocaleList would change if Locale.setDefault() is called. This method
- * takes that into account by always checking the output of Locale.getDefault() and adjusting
- * the default LocaleList if needed.
+ * takes that into account by always checking the output of Locale.getDefault() and
+ * recalculating the default LocaleList if needed.
*/
@NonNull @Size(min=1)
public static LocaleList getDefault() {
@@ -426,7 +462,7 @@
sLastDefaultLocale = defaultLocale;
// It's either the first time someone has asked for the default locale list, or
// someone has called Locale.setDefault() since we last set or adjusted the default
- // locale list. So let's adjust the locale list.
+ // locale list. So let's recalculate the locale list.
if (sDefaultLocaleList != null
&& defaultLocale.equals(sDefaultLocaleList.getPrimary())) {
// The default Locale has changed, but it happens to be the first locale in the
@@ -434,6 +470,7 @@
return sDefaultLocaleList;
}
sDefaultLocaleList = new LocaleList(defaultLocale, sLastExplicitlySetLocaleList);
+ sDefaultAdjustedLocaleList = sDefaultLocaleList;
}
// sDefaultLocaleList can't be null, since it can't be set to null by
// LocaleList.setDefault(), and if getDefault() is called before a call to
@@ -444,6 +481,20 @@
}
/**
+ * Returns the default locale list, adjusted by moving the default locale to its first
+ * position.
+ *
+ * {@hide}
+ */
+ @NonNull @Size(min=1)
+ public static LocaleList getAdjustedDefault() {
+ getDefault(); // to recalculate the default locale list, if necessary
+ synchronized (sLock) {
+ return sDefaultAdjustedLocaleList;
+ }
+ }
+
+ /**
* Also sets the default locale by calling Locale.setDefault() with the first locale in the
* list.
*
@@ -474,6 +525,12 @@
Locale.setDefault(sLastDefaultLocale);
sLastExplicitlySetLocaleList = locales;
sDefaultLocaleList = locales;
+ if (localeIndex == 0) {
+ sDefaultAdjustedLocaleList = sDefaultLocaleList;
+ } else {
+ sDefaultAdjustedLocaleList = new LocaleList(
+ sLastDefaultLocale, sDefaultLocaleList);
+ }
}
}
}
diff --git a/core/tests/coretests/src/android/content/res/ResourcesLocaleResolutionTest.java b/core/tests/coretests/src/android/content/res/ResourcesLocaleResolutionTest.java
deleted file mode 100644
index 55c0031..0000000
--- a/core/tests/coretests/src/android/content/res/ResourcesLocaleResolutionTest.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
-* Copyright (C) 2015 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 android.content.res;
-
-import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.SmallTest;
-import android.util.DisplayMetrics;
-import android.util.LocaleList;
-
-import java.util.Arrays;
-import java.util.Locale;
-
-public class ResourcesLocaleResolutionTest extends AndroidTestCase {
- @SmallTest
- public void testGetResolvedLocale_englishIsAlwaysConsideredSupported() {
- // First make sure English has no explicit assets other than the default assets
- final AssetManager assets = getContext().getAssets();
- final String supportedLocales[] = assets.getNonSystemLocales();
- for (String languageTag : supportedLocales) {
- if ("en-XA".equals(languageTag)) {
- continue;
- }
- assertFalse(
- "supported locales: " + Arrays.toString(supportedLocales),
- "en".equals(Locale.forLanguageTag(languageTag).getLanguage()));
- }
-
- final DisplayMetrics dm = new DisplayMetrics();
- dm.setToDefaults();
- final Configuration cfg = new Configuration();
- cfg.setToDefaults();
- // Avestan and English have no assets, but Persian does.
- cfg.setLocales(LocaleList.forLanguageTags("ae,en,fa"));
- Resources res = new Resources(assets, dm, cfg);
- // We should get English, because it is always considered supported.
- assertEquals("en", res.getResolvedLocale().toLanguageTag());
- }
-}
-