diff options
-rw-r--r-- | Android.bp | 1 | ||||
-rw-r--r-- | core/java/com/android/internal/app/LocalePicker.java | 64 | ||||
-rw-r--r-- | core/sysprop/Android.bp | 21 | ||||
-rw-r--r-- | core/sysprop/LocalizationProperties.sysprop | 24 | ||||
-rw-r--r-- | core/sysprop/api/com.android.sysprop.localization-current.txt | 9 | ||||
-rw-r--r-- | core/sysprop/api/com.android.sysprop.localization-latest.txt | 9 | ||||
-rw-r--r-- | tests/LocalizationTest/Android.bp | 41 | ||||
-rw-r--r-- | tests/LocalizationTest/AndroidManifest.xml | 29 | ||||
-rw-r--r-- | tests/LocalizationTest/AndroidTest.xml | 34 | ||||
-rw-r--r-- | tests/LocalizationTest/java/com/android/internal/app/LocalizationTest.kt | 118 |
10 files changed, 349 insertions, 1 deletions
diff --git a/Android.bp b/Android.bp index bcac22d76424..44c9d268a9a6 100644 --- a/Android.bp +++ b/Android.bp @@ -460,6 +460,7 @@ java_library { "com.android.sysprop.apex", "com.android.sysprop.init", + "com.android.sysprop.localization", "PlatformProperties", ], sdk_version: "core_platform", diff --git a/core/java/com/android/internal/app/LocalePicker.java b/core/java/com/android/internal/app/LocalePicker.java index 3343593f2bc3..0c43578a89cf 100644 --- a/core/java/com/android/internal/app/LocalePicker.java +++ b/core/java/com/android/internal/app/LocalePicker.java @@ -16,6 +16,8 @@ package com.android.internal.app; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.ActivityManager; import android.app.IActivityManager; import android.app.ListFragment; @@ -28,6 +30,7 @@ import android.os.Bundle; import android.os.LocaleList; import android.os.RemoteException; import android.provider.Settings; +import android.sysprop.LocalizationProperties; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -43,6 +46,9 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; public class LocalePicker extends ListFragment { private static final String TAG = "LocalePicker"; @@ -92,7 +98,38 @@ public class LocalePicker extends ListFragment { } public static String[] getSupportedLocales(Context context) { - return context.getResources().getStringArray(R.array.supported_locales); + String[] allLocales = context.getResources().getStringArray(R.array.supported_locales); + + Predicate<String> localeFilter = getLocaleFilter(); + if (localeFilter == null) { + return allLocales; + } + + List<String> result = new ArrayList<>(allLocales.length); + for (String locale : allLocales) { + if (localeFilter.test(locale)) { + result.add(locale); + } + } + + int localeCount = result.size(); + return (localeCount == allLocales.length) ? allLocales + : result.toArray(new String[localeCount]); + } + + @Nullable + private static Predicate<String> getLocaleFilter() { + try { + return LocalizationProperties.locale_filter() + .map(filter -> Pattern.compile(filter).asPredicate()) + .orElse(null); + } catch (SecurityException e) { + Log.e(TAG, "Failed to read locale filter.", e); + } catch (PatternSyntaxException e) { + Log.e(TAG, "Bad locale filter format (\"" + e.getPattern() + "\"), skipping."); + } + + return null; } public static List<LocaleInfo> getAllAssetLocales(Context context, boolean isInDeveloperMode) { @@ -265,6 +302,11 @@ public class LocalePicker extends ListFragment { */ @UnsupportedAppUsage public static void updateLocales(LocaleList locales) { + if (locales != null) { + locales = removeExcludedLocales(locales); + } + // Note: the empty list case is covered by Configuration.setLocales(). + try { final IActivityManager am = ActivityManager.getService(); final Configuration config = am.getConfiguration(); @@ -280,6 +322,26 @@ public class LocalePicker extends ListFragment { } } + @NonNull + private static LocaleList removeExcludedLocales(@NonNull LocaleList locales) { + Predicate<String> localeFilter = getLocaleFilter(); + if (localeFilter == null) { + return locales; + } + + int localeCount = locales.size(); + ArrayList<Locale> filteredLocales = new ArrayList<>(localeCount); + for (int i = 0; i < localeCount; ++i) { + Locale locale = locales.get(i); + if (localeFilter.test(locale.toString())) { + filteredLocales.add(locale); + } + } + + return (localeCount == filteredLocales.size()) ? locales + : new LocaleList(filteredLocales.toArray(new Locale[0])); + } + /** * Get the locale list. * diff --git a/core/sysprop/Android.bp b/core/sysprop/Android.bp new file mode 100644 index 000000000000..7f20a0ba6642 --- /dev/null +++ b/core/sysprop/Android.bp @@ -0,0 +1,21 @@ +// Copyright (C) 2020 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. + +sysprop_library { + name: "com.android.sysprop.localization", + srcs: ["LocalizationProperties.sysprop"], + property_owner: "Platform", + api_packages: ["android.sysprop"], + vendor_available: false, +} diff --git a/core/sysprop/LocalizationProperties.sysprop b/core/sysprop/LocalizationProperties.sysprop new file mode 100644 index 000000000000..65f544fa6179 --- /dev/null +++ b/core/sysprop/LocalizationProperties.sysprop @@ -0,0 +1,24 @@ +# Copyright (C) 2020 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. + +module: "android.sysprop.LocalizationProperties" +owner: Platform + +prop { + api_name: "locale_filter" + type: String + prop_name: "ro.localization.locale_filter" + scope: Internal + access: Readonly +} diff --git a/core/sysprop/api/com.android.sysprop.localization-current.txt b/core/sysprop/api/com.android.sysprop.localization-current.txt new file mode 100644 index 000000000000..fe4f4578683c --- /dev/null +++ b/core/sysprop/api/com.android.sysprop.localization-current.txt @@ -0,0 +1,9 @@ +props { + module: "android.sysprop.LocalizationProperties" + prop { + api_name: "locale_filter" + type: String + scope: Internal + prop_name: "ro.localization.locale_filter" + } +} diff --git a/core/sysprop/api/com.android.sysprop.localization-latest.txt b/core/sysprop/api/com.android.sysprop.localization-latest.txt new file mode 100644 index 000000000000..fe4f4578683c --- /dev/null +++ b/core/sysprop/api/com.android.sysprop.localization-latest.txt @@ -0,0 +1,9 @@ +props { + module: "android.sysprop.LocalizationProperties" + prop { + api_name: "locale_filter" + type: String + scope: Internal + prop_name: "ro.localization.locale_filter" + } +} diff --git a/tests/LocalizationTest/Android.bp b/tests/LocalizationTest/Android.bp new file mode 100644 index 000000000000..c4bfcb1d2261 --- /dev/null +++ b/tests/LocalizationTest/Android.bp @@ -0,0 +1,41 @@ +// Copyright (C) 2020 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. + +android_test { + name: "LocalizationTest", + srcs: ["java/**/*.kt"], + libs: [ + "android.test.runner", + "android.test.base", + "android.test.mock", + ], + static_libs: [ + "androidx.test.core", + "androidx.test.ext.junit", + "androidx.test.rules", + "mockito-target-extended-minus-junit4", + "truth-prebuilt", + ], + jni_libs: [ + // For mockito extended + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + ], + certificate: "platform", + platform_apis: true, + test_suites: ["device-tests"], + optimize: { + enabled: false, + }, +} diff --git a/tests/LocalizationTest/AndroidManifest.xml b/tests/LocalizationTest/AndroidManifest.xml new file mode 100644 index 000000000000..b135443960f5 --- /dev/null +++ b/tests/LocalizationTest/AndroidManifest.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 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. + --> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.android.internal.app"> + + <application android:debuggable="true" android:testOnly="true"> + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.android.internal.app" + android:label="Localization Tests" /> + +</manifest> diff --git a/tests/LocalizationTest/AndroidTest.xml b/tests/LocalizationTest/AndroidTest.xml new file mode 100644 index 000000000000..8309b4f611f8 --- /dev/null +++ b/tests/LocalizationTest/AndroidTest.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 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. + --> +<configuration description="Localization Tests."> + <option name="test-suite-tag" value="apct" /> + <option name="test-suite-tag" value="apct-instrumentation" /> + + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="install-arg" value="-t" /> + <option name="test-file-name" value="LocalizationTest.apk" /> + </target_preparer> + + <option name="test-tag" value="LocalizationTest" /> + + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.android.internal.app" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="hidden-api-checks" value="false"/> + </test> +</configuration>
\ No newline at end of file diff --git a/tests/LocalizationTest/java/com/android/internal/app/LocalizationTest.kt b/tests/LocalizationTest/java/com/android/internal/app/LocalizationTest.kt new file mode 100644 index 000000000000..22ea97167326 --- /dev/null +++ b/tests/LocalizationTest/java/com/android/internal/app/LocalizationTest.kt @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2020 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.android.internal.app + +import android.os.SystemProperties +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn +import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession +import com.android.internal.R +import com.android.internal.app.LocalePicker +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.After +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.eq +import org.mockito.MockitoSession + +@RunWith(AndroidJUnit4::class) +class LocalizationTest { + private val mContext = InstrumentationRegistry.getInstrumentation().context + private val mUnfilteredLocales = + mContext.getResources().getStringArray(R.array.supported_locales) + + private lateinit var mMockitoSession: MockitoSession + + @Before + fun setUp() { + mMockitoSession = mockitoSession() + .initMocks(this) + .spyStatic(SystemProperties::class.java) + .startMocking() + } + + @After + fun tearDown() { + mMockitoSession.finishMocking() + } + + @Test + fun testGetSupportedLocales_noFilter() { + // Filter not set. + setTestLocaleFilter(null) + + val locales1 = LocalePicker.getSupportedLocales(mContext) + + assertThat(locales1).isEqualTo(mUnfilteredLocales) + + // Empty filter. + setTestLocaleFilter("") + + val locales2 = LocalePicker.getSupportedLocales(mContext) + + assertThat(locales2).isEqualTo(mUnfilteredLocales) + } + + @Test + fun testGetSupportedLocales_invalidFilter() { + setTestLocaleFilter("**") + + val locales = LocalePicker.getSupportedLocales(mContext) + + assertThat(locales).isEqualTo(mUnfilteredLocales) + } + + @Test + fun testGetSupportedLocales_inclusiveFilter() { + setTestLocaleFilter("^(de-AT|de-DE|en|ru).*") + + val locales = LocalePicker.getSupportedLocales(mContext) + + assertThat(locales).isEqualTo( + mUnfilteredLocales + .filter { it.startsWithAnyOf("de-AT", "de-DE", "en", "ru") } + .toTypedArray() + ) + } + + @Test + fun testGetSupportedLocales_exclusiveFilter() { + setTestLocaleFilter("^(?!de-IT|es|fr).*") + + val locales = LocalePicker.getSupportedLocales(mContext) + + assertThat(locales).isEqualTo( + mUnfilteredLocales + .filter { !it.startsWithAnyOf("de-IT", "es", "fr") } + .toTypedArray() + ) + } + + private fun setTestLocaleFilter(localeFilter: String?) { + doReturn(localeFilter).`when` { SystemProperties.get(eq("ro.localization.locale_filter")) } + } + + private fun String.startsWithAnyOf(vararg prefixes: String): Boolean { + prefixes.forEach { + if (startsWith(it)) return true + } + + return false + } +} |