Merge "Integrate system language filtering functionality" am: 2df94f9b15 am: db00e424d9

Original change: https://android-review.googlesource.com/c/platform/frameworks/base/+/1362798

Change-Id: I4684de9e0199b2d750108473366d04f605b6e955
This commit is contained in:
Alexander Mishkovets
2020-07-30 12:18:53 +00:00
committed by Automerger Merge Worker
10 changed files with 349 additions and 1 deletions

View File

@@ -460,6 +460,7 @@ java_library {
"com.android.sysprop.apex",
"com.android.sysprop.init",
"com.android.sysprop.localization",
"PlatformProperties",
],
sdk_version: "core_platform",

View File

@@ -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.
*

21
core/sysprop/Android.bp Normal file
View File

@@ -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,
}

View File

@@ -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
}

View File

@@ -0,0 +1,9 @@
props {
module: "android.sysprop.LocalizationProperties"
prop {
api_name: "locale_filter"
type: String
scope: Internal
prop_name: "ro.localization.locale_filter"
}
}

View File

@@ -0,0 +1,9 @@
props {
module: "android.sysprop.LocalizationProperties"
prop {
api_name: "locale_filter"
type: String
scope: Internal
prop_name: "ro.localization.locale_filter"
}
}

View File

@@ -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,
},
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
}
}