From b61e405397200f78b1c652143cba7c751df05a00 Mon Sep 17 00:00:00 2001 From: Adam Lesinski Date: Thu, 19 May 2016 18:23:05 -0700 Subject: [PATCH] Improve performance of LocaleList with Resources We allow each individual Resources object to select the best Locale for the given APK. This allows one update to the configuration instead of multiple updates, once the locale is chosen. The Java locale is selected from the app context's locale. Bug:28625993 Bug:27325465 Change-Id: I99e1e53f522e560f3b80bbd1e1c605f552dbdff0 --- core/java/android/app/ActivityThread.java | 64 ++++++++---- core/java/android/app/ResourcesManager.java | 74 +------------- .../android/content/res/Configuration.java | 2 +- .../android/content/res/ResourcesImpl.java | 31 +++++- core/java/android/os/LocaleList.java | 14 ++- core/tests/coretests/apks/locales/Android.mk | 8 ++ .../apks/locales/AndroidManifest.xml | 21 ++++ .../apks/locales/res/values-pl/values.xml | 18 ++++ .../apks/locales/res/values/values.xml | 18 ++++ .../content/res/ResourcesLocaleTest.java | 97 +++++++++++++++++++ .../server/am/ActivityManagerService.java | 18 ++-- 11 files changed, 262 insertions(+), 103 deletions(-) create mode 100644 core/tests/coretests/apks/locales/Android.mk create mode 100644 core/tests/coretests/apks/locales/AndroidManifest.xml create mode 100644 core/tests/coretests/apks/locales/res/values-pl/values.xml create mode 100644 core/tests/coretests/apks/locales/res/values/values.xml create mode 100644 core/tests/coretests/src/android/content/res/ResourcesLocaleTest.java diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index dff07693c82fb..30753c1632ff3 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -62,6 +62,7 @@ import android.os.DropBoxManager; import android.os.Environment; import android.os.Handler; import android.os.IBinder; +import android.os.LocaleList; import android.os.Looper; import android.os.Message; import android.os.MessageQueue; @@ -130,6 +131,7 @@ import java.text.DateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.TimeZone; @@ -4684,6 +4686,8 @@ public final class ActivityThread { + config); mResourcesManager.applyConfigurationToResourcesLocked(config, compat); + updateLocaleListFromAppContext(mInitialApplication.getApplicationContext(), + mResourcesManager.getConfiguration().getLocales()); if (mConfiguration == null) { mConfiguration = new Configuration(); @@ -4989,6 +4993,24 @@ public final class ActivityThread { return insInfo.nativeLibraryDir; } + /** + * The LocaleList set for the app's resources may have been shuffled so that the preferred + * Locale is at position 0. We must find the index of this preferred Locale in the + * original LocaleList. + */ + private void updateLocaleListFromAppContext(Context context, LocaleList newLocaleList) { + final Locale bestLocale = context.getResources().getConfiguration().getLocales().get(0); + final int newLocaleListSize = newLocaleList.size(); + for (int i = 0; i < newLocaleListSize; i++) { + if (bestLocale.equals(newLocaleList.get(i))) { + LocaleList.setDefault(newLocaleList, i); + return; + } + } + throw new AssertionError("chosen locale " + bestLocale + " must be present in LocaleList: " + + newLocaleList.toLanguageTags()); + } + private void handleBindApplication(AppBindData data) { // Register the UI Thread as a sensitive thread to the runtime. VMRuntime.registerSensitiveThread(); @@ -5047,6 +5069,24 @@ public final class ActivityThread { */ TimeZone.setDefault(null); + /* + * Set the LocaleList. This may change once we create the App Context. + */ + LocaleList.setDefault(data.config.getLocales()); + + synchronized (mResourcesManager) { + /* + * Update the system configuration since its preloaded and might not + * reflect configuration changes. The configuration object passed + * in AppBindData can be safely assumed to be up to date + */ + mResourcesManager.applyConfigurationToResourcesLocked(data.config, data.compatInfo); + mCurDefaultDisplayDpi = data.config.densityDpi; + + // This calls mResourcesManager so keep it within the synchronized block. + applyCompatConfiguration(mCurDefaultDisplayDpi); + } + data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo); /** @@ -5174,25 +5214,8 @@ public final class ActivityThread { } final ContextImpl appContext = ContextImpl.createAppContext(this, data.info); - synchronized (mResourcesManager) { - /* - * 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. - */ - mResourcesManager.setDefaultLocalesLocked(data.config.getLocales()); - - /* - * Update the system configuration since its preloaded and might not - * reflect configuration changes. The configuration object passed - * in AppBindData can be safely assumed to be up to date - */ - mResourcesManager.applyConfigurationToResourcesLocked(data.config, data.compatInfo); - mCurDefaultDisplayDpi = data.config.densityDpi; - - // This calls mResourcesManager so keep it within the synchronized block. - applyCompatConfiguration(mCurDefaultDisplayDpi); - } + updateLocaleListFromAppContext(appContext, + mResourcesManager.getConfiguration().getLocales()); if (!Process.isIsolated() && !"android".equals(appContext.getPackageName())) { // This cache location probably points at credential-encrypted @@ -5895,6 +5918,9 @@ public final class ActivityThread { // immediately, because upon returning the view // hierarchy will be informed about it. if (mResourcesManager.applyConfigurationToResourcesLocked(newConfig, null)) { + updateLocaleListFromAppContext(mInitialApplication.getApplicationContext(), + mResourcesManager.getConfiguration().getLocales()); + // This actually changed the resources! Tell // everyone about it. if (mPendingConfiguration == null || diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java index b4e9db86d18c1..25a8b66a42aad 100644 --- a/core/java/android/app/ResourcesManager.java +++ b/core/java/android/app/ResourcesManager.java @@ -67,10 +67,6 @@ public class ResourcesManager { } }; - private String[] mSystemLocales = null; - private final HashSet mNonSystemLocales = new HashSet<>(); - private boolean mHasNonSystemLocales = false; - /** * The global compatibility settings. */ @@ -479,12 +475,7 @@ public class ResourcesManager { */ private Resources getOrCreateResources(@Nullable IBinder activityToken, @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) { - final boolean findSystemLocales; - final boolean hasNonSystemLocales; synchronized (this) { - findSystemLocales = (mSystemLocales == null || mSystemLocales.length == 0); - hasNonSystemLocales = mHasNonSystemLocales; - if (DEBUG) { Throwable here = new Throwable(); here.fillInStackTrace(); @@ -538,24 +529,7 @@ public class ResourcesManager { // If we're here, we didn't find a suitable ResourcesImpl to use, so create one now. ResourcesImpl resourcesImpl = createResourcesImpl(key); - final String[] systemLocales = findSystemLocales - ? AssetManager.getSystem().getLocales() : null; - final String[] nonSystemLocales = resourcesImpl.getAssets().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) { - if (mSystemLocales == null || mSystemLocales.length == 0) { - mSystemLocales = systemLocales; - } - mNonSystemLocales.addAll(Arrays.asList(nonSystemLocales)); - mHasNonSystemLocales = mHasNonSystemLocales || !isPseudoLocalesOnly; - ResourcesImpl existingResourcesImpl = findResourcesImplForKeyLocked(key); if (existingResourcesImpl != null) { if (DEBUG) { @@ -745,23 +719,6 @@ public class ResourcesManager { } } - /* package */ void setDefaultLocalesLocked(@NonNull LocaleList locales) { - if (mSystemLocales == null) { - throw new RuntimeException("ResourcesManager is not ready to negotiate 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); - } - public final boolean applyConfigurationToResourcesLocked(@NonNull Configuration config, @Nullable CompatibilityInfo compat) { try { @@ -786,30 +743,7 @@ public class ResourcesManager { | ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE; } - 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(localeAdjustedConfig, defaultDisplayMetrics, - compat); + Resources.updateSystemConfiguration(config, defaultDisplayMetrics, compat); ApplicationPackageManager.configurationChanged(); //Slog.i(TAG, "Configuration changed in " + currentPackageName()); @@ -821,7 +755,7 @@ public class ResourcesManager { ResourcesImpl r = mResourceImpls.valueAt(i).get(); if (r != null) { if (DEBUG || DEBUG_CONFIGURATION) Slog.v(TAG, "Changing resources " - + r + " config to: " + localeAdjustedConfig); + + r + " config to: " + config); int displayId = key.mDisplayId; boolean isDefaultDisplay = (displayId == Display.DEFAULT_DISPLAY); DisplayMetrics dm = defaultDisplayMetrics; @@ -830,7 +764,7 @@ public class ResourcesManager { if (tmpConfig == null) { tmpConfig = new Configuration(); } - tmpConfig.setTo(localeAdjustedConfig); + tmpConfig.setTo(config); if (!isDefaultDisplay) { dm = getDisplayMetrics(displayId); applyNonDefaultDisplayMetricsToConfiguration(dm, tmpConfig); @@ -840,7 +774,7 @@ public class ResourcesManager { } r.updateConfiguration(tmpConfig, dm, compat); } else { - r.updateConfiguration(localeAdjustedConfig, dm, compat); + r.updateConfiguration(config, dm, compat); } //Slog.i(TAG, "Updated app resources " + v.getKey() // + " " + r + ": " + r.getConfiguration()); diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java index c1aac8584acb2..f6445e6cd0902 100644 --- a/core/java/android/content/res/Configuration.java +++ b/core/java/android/content/res/Configuration.java @@ -1446,7 +1446,7 @@ public final class Configuration implements Parcelable, Comparable 1) { + // The LocaleList has changed. We must query the AssetManager's available + // Locales and figure out the best matching Locale in the new LocaleList. + String[] availableLocales = mAssets.getNonSystemLocales(); + if (LocaleList.isPseudoLocalesOnly(availableLocales)) { + // No app defined locales, so grab the system locales. + availableLocales = mAssets.getLocales(); + if (LocaleList.isPseudoLocalesOnly(availableLocales)) { + availableLocales = null; + } + } + + if (availableLocales != null) { + final Locale bestLocale = locales.getFirstMatchWithEnglishSupported( + availableLocales); + if (bestLocale != null && bestLocale != locales.get(0)) { + mConfiguration.setLocales(new LocaleList(bestLocale, locales)); + } + } + } + } + if (mConfiguration.densityDpi != Configuration.DENSITY_DPI_UNDEFINED) { mMetrics.densityDpi = mConfiguration.densityDpi; mMetrics.density = @@ -370,7 +395,7 @@ public class ResourcesImpl { } mAssets.setConfiguration(mConfiguration.mcc, mConfiguration.mnc, - adjustLanguageTag(locales.get(0).toLanguageTag()), + adjustLanguageTag(mConfiguration.getLocales().get(0).toLanguageTag()), mConfiguration.orientation, mConfiguration.touchscreen, mConfiguration.densityDpi, mConfiguration.keyboard, diff --git a/core/java/android/os/LocaleList.java b/core/java/android/os/LocaleList.java index 8136796574e0b..60b618a2f689b 100644 --- a/core/java/android/os/LocaleList.java +++ b/core/java/android/os/LocaleList.java @@ -408,6 +408,14 @@ public final class LocaleList implements Parcelable { false /* assume English is not supported */); } + /** + * {@hide} + */ + public int getFirstMatchIndex(String[] supportedLocales) { + return computeFirstMatchIndex(Arrays.asList(supportedLocales), + false /* assume English is not supported */); + } + /** * Same as getFirstMatch(), but with English assumed to be supported, even if it's not. * {@hide} @@ -437,7 +445,11 @@ public final class LocaleList implements Parcelable { * Assumes that there is no repetition in the input. * {@hide} */ - public static boolean isPseudoLocalesOnly(String[] supportedLocales) { + public static boolean isPseudoLocalesOnly(@Nullable String[] supportedLocales) { + if (supportedLocales == null) { + return true; + } + 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 diff --git a/core/tests/coretests/apks/locales/Android.mk b/core/tests/coretests/apks/locales/Android.mk new file mode 100644 index 0000000000000..9cb13dd4cd0ee --- /dev/null +++ b/core/tests/coretests/apks/locales/Android.mk @@ -0,0 +1,8 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +LOCAL_PACKAGE_NAME := locales + +include $(FrameworkCoreTests_BUILD_PACKAGE) diff --git a/core/tests/coretests/apks/locales/AndroidManifest.xml b/core/tests/coretests/apks/locales/AndroidManifest.xml new file mode 100644 index 0000000000000..8a1f575510e73 --- /dev/null +++ b/core/tests/coretests/apks/locales/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/core/tests/coretests/apks/locales/res/values-pl/values.xml b/core/tests/coretests/apks/locales/res/values-pl/values.xml new file mode 100644 index 0000000000000..22237fa81001e --- /dev/null +++ b/core/tests/coretests/apks/locales/res/values-pl/values.xml @@ -0,0 +1,18 @@ + + + + Text (pl) + diff --git a/core/tests/coretests/apks/locales/res/values/values.xml b/core/tests/coretests/apks/locales/res/values/values.xml new file mode 100644 index 0000000000000..cb990d20215ed --- /dev/null +++ b/core/tests/coretests/apks/locales/res/values/values.xml @@ -0,0 +1,18 @@ + + + + Text + diff --git a/core/tests/coretests/src/android/content/res/ResourcesLocaleTest.java b/core/tests/coretests/src/android/content/res/ResourcesLocaleTest.java new file mode 100644 index 0000000000000..e85666eb16399 --- /dev/null +++ b/core/tests/coretests/src/android/content/res/ResourcesLocaleTest.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2016 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.os.FileUtils; +import android.os.LocaleList; +import android.support.test.filters.SmallTest; +import android.test.AndroidTestCase; +import android.util.DisplayMetrics; +import android.view.Display; + +import com.android.frameworks.coretests.R; + +import java.io.File; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Locale; + +public class ResourcesLocaleTest extends AndroidTestCase { + + private String extractApkAndGetPath(int id) throws Exception { + final Resources resources = getContext().getResources(); + try (InputStream is = resources.openRawResource(id)) { + File path = new File(getContext().getFilesDir(), resources.getResourceEntryName(id)); + FileUtils.copyToFileOrThrow(is, path); + return path.getAbsolutePath(); + } + } + + private Resources createResourcesWithApk(int rawApkId) throws Exception { + final AssetManager assets = new AssetManager(); + assertTrue(assets.addAssetPath(extractApkAndGetPath(rawApkId)) != 0); + + final DisplayMetrics dm = new DisplayMetrics(); + dm.setToDefaults(); + return new Resources(assets, dm, new Configuration()); + } + + private static void ensureNoLanguage(Resources resources, String language) { + final String[] supportedLocales = resources.getAssets().getNonSystemLocales(); + for (String languageTag : supportedLocales) { + if ("en-XA".equals(languageTag)) { + continue; + } + assertFalse( + "supported locales: " + Arrays.toString(supportedLocales), + language.equals(Locale.forLanguageTag(languageTag).getLanguage())); + } + } + + @SmallTest + public void testEnglishIsAlwaysConsideredSupported() throws Exception { + final Resources resources = createResourcesWithApk(R.raw.locales); + ensureNoLanguage(resources, "en"); + + final LocaleList preferredLocales = LocaleList.forLanguageTags("en-US,pl-PL"); + final Configuration config = new Configuration(); + config.setLocales(preferredLocales); + + resources.updateConfiguration(config, null); + + // The APK we loaded has default and Polish languages. If English is first in the list, + // always take it the default (assumed to be English). + assertEquals(Locale.forLanguageTag("en-US"), + resources.getConfiguration().getLocales().get(0)); + } + + @SmallTest + public void testSelectFirstSupportedLanguage() throws Exception { + final Resources resources = createResourcesWithApk(R.raw.locales); + ensureNoLanguage(resources, "fr"); + + final LocaleList preferredLocales = LocaleList.forLanguageTags("fr-FR,pl-PL"); + final Configuration config = new Configuration(); + config.setLocales(preferredLocales); + + resources.updateConfiguration(config, null); + + // The APK we loaded has default and Polish languages. We expect the Polish language to + // therefore be chosen. + assertEquals(Locale.forLanguageTag("pl-PL"), + resources.getConfiguration().getLocales().get(0)); + } +} diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 7a59703476672..b80487f78d9cc 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -18325,21 +18325,21 @@ public final class ActivityManagerService extends ActivityManagerNative EventLog.writeEvent(EventLogTags.CONFIGURATION_CHANGED, changes); if (!initLocale && !values.getLocales().isEmpty() && values.userSetLocale) { - final Locale locale; - if (values.getLocales().size() == 1) { - // This is an optimization to avoid the JNI call when the result of - // getFirstMatch() does not depend on the supported locales. - locale = values.getLocales().get(0); - } else { + final LocaleList locales = values.getLocales(); + int bestLocaleIndex = 0; + if (locales.size() > 1) { if (mSupportedSystemLocales == null) { mSupportedSystemLocales = Resources.getSystem().getAssets().getLocales(); } - locale = values.getLocales().getFirstMatch(mSupportedSystemLocales); + bestLocaleIndex = Math.max(0, + locales.getFirstMatchIndex(mSupportedSystemLocales)); } - SystemProperties.set("persist.sys.locale", locale.toLanguageTag()); + SystemProperties.set("persist.sys.locale", + locales.get(bestLocaleIndex).toLanguageTag()); + LocaleList.setDefault(locales, bestLocaleIndex); mHandler.sendMessage(mHandler.obtainMessage(SEND_LOCALE_TO_MOUNT_DAEMON_MSG, - locale)); + locales.get(bestLocaleIndex))); } mConfigurationSeq++;