From 0f19cc779fb81bca0d00fd0a062f431cedb5f684 Mon Sep 17 00:00:00 2001 From: Seigo Nonaka Date: Mon, 26 Jun 2017 16:06:17 -0700 Subject: Restore device locale from backup. The locales are merged with following policy. - Don't change UI locale. - Don't add unsupported locales. - Don't add duplicated locales. Bug: 35391006 Test: com.android.providers.settings.SettingsHelperTest Test: Did the following tests manually. 1. Login with Google account during SUW. 2. Set locale to "zh-TW,en-US" 3. adb shell bmgr backupnow com.android.providers.settings 4. fastboot flash userdata && fastboot reboot 5. adb reboot bootloader 6. fastboot flash userdata && fastboot reboot 7. Choose "Japanese" as the first menu on the SUW. 8. Backup from cloud with logging in to the Google account. 9. After compete SUW, verify the device locale is "ja-JP,zh-TW,en-US" Change-Id: I1e6c7ba5b7abb6bde8b01ce0f647c04a5caa81a6 --- .../android/providers/settings/SettingsHelper.java | 123 ++++++++++++++----- packages/SettingsProvider/test/Android.mk | 5 +- .../providers/settings/SettingsHelperTest.java | 135 +++++++++++++++++++++ 3 files changed, 228 insertions(+), 35 deletions(-) create mode 100644 packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java index 8abdc641d675..6247b3bd4994 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java @@ -16,6 +16,11 @@ package com.android.providers.settings; +import com.android.internal.R; +import com.android.internal.app.LocalePicker; +import com.android.internal.annotations.VisibleForTesting; + +import android.annotation.NonNull; import android.app.ActivityManager; import android.app.IActivityManager; import android.app.backup.IBackupManager; @@ -24,11 +29,13 @@ import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; +import android.icu.util.ULocale; import android.location.LocationManager; import android.media.AudioManager; import android.media.RingtoneManager; import android.net.Uri; import android.os.IPowerManager; +import android.os.LocaleList; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; @@ -39,6 +46,9 @@ import android.text.TextUtils; import android.util.ArraySet; import android.util.Slog; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; import java.util.Locale; public class SettingsHelper { @@ -305,59 +315,106 @@ public class SettingsHelper { } } - byte[] getLocaleData() { + /* package */ byte[] getLocaleData() { Configuration conf = mContext.getResources().getConfiguration(); - final Locale loc = conf.locale; - String localeString = loc.getLanguage(); - String country = loc.getCountry(); - if (!TextUtils.isEmpty(country)) { - localeString += "-" + country; + return conf.getLocales().toLanguageTags().getBytes(); + } + + private static Locale toFullLocale(@NonNull Locale locale) { + if (locale.getScript().isEmpty() || locale.getCountry().isEmpty()) { + return ULocale.addLikelySubtags(ULocale.forLocale(locale)).toLocale(); } - return localeString.getBytes(); + return locale; } /** - * Sets the locale specified. Input data is the byte representation of a - * BCP-47 language tag. For backwards compatibility, strings of the form + * Merging the locale came from backup server and current device locale. + * + * Merge works with following rules. + * - The backup locales are appended to the current locale with keeping order. + * e.g. current locale "en-US,zh-CN" and backup locale "ja-JP,ko-KR" are merged to + * "en-US,zh-CH,ja-JP,ko-KR". + * + * - Duplicated locales are dropped. + * e.g. current locale "en-US,zh-CN" and backup locale "ja-JP,zh-Hans-CN,en-US" are merged to + * "en-US,zh-CN,ja-JP". + * + * - Unsupported locales are dropped. + * e.g. current locale "en-US" and backup locale "ja-JP,zh-CN" but the supported locales + * are "en-US,zh-CN", the merged locale list is "en-US,zh-CN". + * + * - The final result locale list only contains the supported locales. + * e.g. current locale "en-US" and backup locale "zh-Hans-CN" and supported locales are + * "en-US,zh-CN", the merged locale list is "en-US,zh-CN". + * + * @param restore The locale list that came from backup server. + * @param current The device's locale setting. + * @param supportedLocales The list of language tags supported by this device. + */ + @VisibleForTesting + public static LocaleList resolveLocales(LocaleList restore, LocaleList current, + String[] supportedLocales) { + final HashMap allLocales = new HashMap<>(supportedLocales.length); + for (String supportedLocaleStr : supportedLocales) { + final Locale locale = Locale.forLanguageTag(supportedLocaleStr); + allLocales.put(toFullLocale(locale), locale); + } + + final ArrayList filtered = new ArrayList<>(current.size()); + for (int i = 0; i < current.size(); i++) { + final Locale locale = current.get(i); + allLocales.remove(toFullLocale(locale)); + filtered.add(locale); + } + + for (int i = 0; i < restore.size(); i++) { + final Locale locale = allLocales.remove(toFullLocale(restore.get(i))); + if (locale != null) { + filtered.add(locale); + } + } + + if (filtered.size() == current.size()) { + return current; // Nothing added to current locale list. + } + + return new LocaleList(filtered.toArray(new Locale[filtered.size()])); + } + + /** + * Sets the locale specified. Input data is the byte representation of comma separated + * multiple BCP-47 language tags. For backwards compatibility, strings of the form * {@code ll_CC} are also accepted, where {@code ll} is a two letter language * code and {@code CC} is a two letter country code. * - * @param data the locale string in bytes. + * @param data the comma separated BCP-47 language tags in bytes. */ - void setLocaleData(byte[] data, int size) { - // Check if locale was set by the user: - final ContentResolver cr = mContext.getContentResolver(); - final boolean userSetLocale = mContext.getResources().getConfiguration().userSetLocale; - final boolean provisioned = Settings.Global.getInt(cr, - Settings.Global.DEVICE_PROVISIONED, 0) != 0; - if (userSetLocale || provisioned) { - // Don't change if user set it in the SetupWizard, or if this is a post-setup - // deferred restore operation - Slog.i(TAG, "Not applying restored locale; " - + (userSetLocale ? "user already specified" : "device already provisioned")); + /* package */ void setLocaleData(byte[] data, int size) { + final Configuration conf = mContext.getResources().getConfiguration(); + + // Replace "_" with "-" to deal with older backups. + final String localeCodes = new String(data, 0, size).replace('_', '-'); + final LocaleList localeList = LocaleList.forLanguageTags(localeCodes); + if (localeList.isEmpty()) { return; } - final String[] availableLocales = mContext.getAssets().getLocales(); - // Replace "_" with "-" to deal with older backups. - String localeCode = new String(data, 0, size).replace('_', '-'); - Locale loc = null; - for (int i = 0; i < availableLocales.length; i++) { - if (availableLocales[i].equals(localeCode)) { - loc = Locale.forLanguageTag(localeCode); - break; - } + final String[] supportedLocales = LocalePicker.getSupportedLocales(mContext); + final LocaleList currentLocales = conf.getLocales(); + + final LocaleList merged = resolveLocales(localeList, currentLocales, supportedLocales); + if (merged.equals(currentLocales)) { + return; } - if (loc == null) return; // Couldn't find the saved locale in this version of the software try { IActivityManager am = ActivityManager.getService(); Configuration config = am.getConfiguration(); - config.locale = loc; + config.setLocales(merged); // indicate this isn't some passing default - the user wants this remembered config.userSetLocale = true; - am.updateConfiguration(config); + am.updatePersistentConfiguration(config); } catch (RemoteException e) { // Intentionally left blank } diff --git a/packages/SettingsProvider/test/Android.mk b/packages/SettingsProvider/test/Android.mk index 85e611f95d78..a9707d4ae69b 100644 --- a/packages/SettingsProvider/test/Android.mk +++ b/packages/SettingsProvider/test/Android.mk @@ -4,10 +4,11 @@ include $(CLEAR_VARS) LOCAL_MODULE_TAGS := tests -# Note we statically link SettingsState to do some unit tests. It's not accessible otherwise +# Note we statically link several classes to do some unit tests. It's not accessible otherwise # because this test is not an instrumentation test. (because the target runs in the system process.) LOCAL_SRC_FILES := $(call all-subdir-java-files) \ - ../src/com/android/providers/settings/SettingsState.java + ../src/com/android/providers/settings/SettingsState.java \ + ../src/com/android/providers/settings/SettingsHelper.java LOCAL_STATIC_JAVA_LIBRARIES := android-support-test diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java new file mode 100644 index 000000000000..6fa014d4bef7 --- /dev/null +++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2017 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.providers.settings; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertSame; +import static junit.framework.Assert.assertNull; +import static junit.framework.Assert.fail; + +import com.android.internal.app.LocalePicker; +import com.android.providers.settings.SettingsHelper; + +import android.os.LocaleList; +import android.support.test.runner.AndroidJUnit4; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Tests for the SettingsHelperTest + */ +@RunWith(AndroidJUnit4.class) +public class SettingsHelperTest { + @Test + public void testResolveLocales() throws Exception { + // Empty string from backup server + assertEquals(LocaleList.forLanguageTags("en-US"), + SettingsHelper.resolveLocales( + LocaleList.forLanguageTags(""), // restore + LocaleList.forLanguageTags("en-US"), // current + new String[] { "en-US" })); // supported + + // Same as current settings + assertEquals(LocaleList.forLanguageTags("en-US"), + SettingsHelper.resolveLocales( + LocaleList.forLanguageTags("en-US"), // restore + LocaleList.forLanguageTags("en-US"), // current + new String[] { "en-US" })); // supported + + assertEquals(LocaleList.forLanguageTags("en-US,ja-JP"), + SettingsHelper.resolveLocales( + LocaleList.forLanguageTags("en-US,ja-JP"), // restore + LocaleList.forLanguageTags("en-US,ja-JP"), // current + new String[] { "en-US", "ja-JP" })); // supported + + // Current locale must be kept at the first place. + assertEquals(LocaleList.forLanguageTags("ja-JP,en-US"), + SettingsHelper.resolveLocales( + LocaleList.forLanguageTags("en-US"), // restore + LocaleList.forLanguageTags("ja-JP"), // current + new String[] { "en-US", "ja-JP" })); // supported + + assertEquals(LocaleList.forLanguageTags("ja-JP,ko-KR,en-US"), + SettingsHelper.resolveLocales( + LocaleList.forLanguageTags("en-US"), // restore + LocaleList.forLanguageTags("ja-JP,ko-KR"), // current + new String[] { "en-US", "ja-JP", "ko-KR" })); // supported + + assertEquals(LocaleList.forLanguageTags("ja-JP,en-US,ko-KR"), + SettingsHelper.resolveLocales( + LocaleList.forLanguageTags("en-US,ko-KR"), // restore + LocaleList.forLanguageTags("ja-JP"), // current + new String[] { "en-US", "ja-JP", "ko-KR" })); // supported + + // Duplicated entries must be removed. + assertEquals(LocaleList.forLanguageTags("ja-JP,ko-KR,en-US"), + SettingsHelper.resolveLocales( + LocaleList.forLanguageTags("en-US,ko-KR"), // restore + LocaleList.forLanguageTags("ja-JP,ko-KR"), // current + new String[] { "en-US", "ja-JP", "ko-KR" })); // supported + + // Drop unsupported locales. + assertEquals(LocaleList.forLanguageTags("en-US"), + SettingsHelper.resolveLocales( + LocaleList.forLanguageTags("en-US,zh-CN"), // restore + LocaleList.forLanguageTags("en-US"), // current + new String[] { "en-US" })); // supported + + // Comparison happens on fully-expanded locale. + assertEquals(LocaleList.forLanguageTags("en-US,sr-Latn-SR,sr-Cryl-SR"), + SettingsHelper.resolveLocales( + LocaleList.forLanguageTags("sr-Cryl-SR"), // restore + LocaleList.forLanguageTags("en-US,sr-Latn-SR"), // current + new String[] { "en-US", "sr-Latn-SR", "sr-Cryl-SR" })); // supported + + assertEquals(LocaleList.forLanguageTags("en-US"), + SettingsHelper.resolveLocales( + LocaleList.forLanguageTags("kk-Cryl-KZ"), // restore + LocaleList.forLanguageTags("en-US"), // current + new String[] { "en-US", "kk-Latn-KZ" })); // supported + + assertEquals(LocaleList.forLanguageTags("en-US,kk-Cryl-KZ"), + SettingsHelper.resolveLocales( + LocaleList.forLanguageTags("kk-Cryl-KZ"), // restore + LocaleList.forLanguageTags("en-US"), // current + new String[] { "en-US", "kk-Cryl-KZ" })); // supported + + assertEquals(LocaleList.forLanguageTags("en-US,zh-CN"), + SettingsHelper.resolveLocales( + LocaleList.forLanguageTags("zh-Hans-CN"), // restore + LocaleList.forLanguageTags("en-US"), // current + new String[] { "en-US", "zh-CN" })); // supported + + assertEquals(LocaleList.forLanguageTags("en-US,zh-Hans-CN"), + SettingsHelper.resolveLocales( + LocaleList.forLanguageTags("zh-CN"), // restore + LocaleList.forLanguageTags("en-US"), // current + new String[] { "en-US", "zh-Hans-CN" })); // supported + + // Old langauge code should be updated. + assertEquals(LocaleList.forLanguageTags("en-US,he-IL,id-ID,yi"), + SettingsHelper.resolveLocales( + LocaleList.forLanguageTags("iw-IL,in-ID,ji"), // restore + LocaleList.forLanguageTags("en-US"), // current + new String[] { "he-IL", "id-ID", "yi" })); // supported + } +} -- cgit v1.2.3-59-g8ed1b