diff options
| -rw-r--r-- | api/current.txt | 1 | ||||
| -rw-r--r-- | api/system-current.txt | 1 | ||||
| -rw-r--r-- | api/test-current.txt | 1 | ||||
| -rw-r--r-- | core/java/android/app/ActivityThread.java | 4 | ||||
| -rw-r--r-- | core/java/android/app/ResourcesManager.java | 75 | ||||
| -rw-r--r-- | core/java/android/content/res/Resources.java | 44 | ||||
| -rw-r--r-- | core/java/android/util/LocaleList.java | 121 | ||||
| -rw-r--r-- | core/tests/coretests/src/android/content/res/ResourcesLocaleResolutionTest.java | 53 |
8 files changed, 164 insertions, 136 deletions
diff --git a/api/current.txt b/api/current.txt index e0b92f4039b5..ae44b40494c3 100644 --- a/api/current.txt +++ b/api/current.txt @@ -10085,7 +10085,6 @@ package android.content.res { 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 6b84f5672608..87e2e21d80cc 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -10480,7 +10480,6 @@ package android.content.res { 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 f0038758ae70..20b0e6392df1 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -10093,7 +10093,6 @@ package android.content.res { 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 7b0195391b47..929bb7242cfc 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -4888,8 +4888,10 @@ public final class ActivityThread { /* * 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 260216cfefb8..94e584eeaa98 100644 --- a/core/java/android/app/ResourcesManager.java +++ b/core/java/android/app/ResourcesManager.java @@ -35,6 +35,9 @@ import android.view.Display; 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 @@ public class ResourcesManager { 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 @@ public class ResourcesManager { ? 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 @@ public class ResourcesManager { + " key=" + key + " overrideConfig=" + overrideConfiguration); return r; } + findSystemLocales = (mSystemLocales.length == 0); + hasNonSystemLocales = mHasNonSystemLocales; } //if (r != null) { @@ -243,6 +254,18 @@ public class ResourcesManager { 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 @@ public class ResourcesManager { // 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 @@ public class ResourcesManager { | 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 @@ public class ResourcesManager { 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 @@ public class ResourcesManager { if (tmpConfig == null) { tmpConfig = new Configuration(); } - tmpConfig.setTo(config); + tmpConfig.setTo(localeAdjustedConfig); if (!isDefaultDisplay) { dm = getDisplayMetricsLocked(displayId); applyNonDefaultDisplayMetricsToConfigurationLocked(dm, tmpConfig); @@ -320,7 +377,7 @@ public class ResourcesManager { } 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 ed64eade0351..e4044296dac4 100644 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -162,10 +162,6 @@ public class Resources { 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 @@ public class Resources { } /** - * 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 @@ public class Resources { private PluralRules getPluralRule() { synchronized (sSync) { if (mPluralRule == null) { - mPluralRule = PluralRules.forLocale(mResolvedLocale); + mPluralRule = PluralRules.forLocale(mConfiguration.getLocales().getPrimary()); } return mPluralRule; } @@ -457,7 +443,7 @@ public class Resources { @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 class Resources { 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 @@ public class Resources { 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 @@ public class Resources { 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 @@ public class Resources { } 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 8a2d015a6f7b..90a20bc2ab24 100644 --- a/core/java/android/util/LocaleList.java +++ b/core/java/android/util/LocaleList.java @@ -26,6 +26,8 @@ import android.os.Parcelable; 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 @@ public final class LocaleList implements Parcelable { 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 @@ public final class LocaleList implements Parcelable { */ @Nullable public Locale getFirstMatchWithEnglishSupported(String[] supportedLocales) { - return computeFirstMatch(supportedLocales, true /* assume English is supported */); + return computeFirstMatch(Arrays.asList(supportedLocales), + true /* assume English is supported */); + } + + /** + * {@hide} + */ + public int getFirstMatchIndexWithEnglishSupported(Collection<String> supportedLocales) { + return computeFirstMatchIndex(supportedLocales, true /* assume English is supported */); } /** - * Returns true if the array of locale tags only contains empty locales and pseudolocales. + * {@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 @@ public final class LocaleList implements Parcelable { 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 @@ public final class LocaleList implements Parcelable { @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 @@ public final class LocaleList implements Parcelable { * 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 @@ public final class LocaleList implements Parcelable { 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 @@ public final class LocaleList implements Parcelable { 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 @@ public final class LocaleList implements Parcelable { } /** + * 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 @@ public final class LocaleList implements Parcelable { 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 55c00314ee63..000000000000 --- 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()); - } -} - |