totally overhaul choosing locales from app metadata based on LocaleList

This makes the selection logic heed the list of preferred locales from the
user Settings.

closes #987
closes #1186
refs #1440 #1882 #1730
!886
This commit is contained in:
Hans-Christoph Steiner
2021-02-05 16:36:47 +01:00
parent fbbf78dcf8
commit e35335d59c
7 changed files with 585 additions and 234 deletions

View File

@@ -6,6 +6,7 @@ import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import androidx.core.os.LocaleListCompat;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.TestUtils;
import org.fdroid.fdroid.data.Schema.AppMetadataTable.Cols;
@@ -303,7 +304,7 @@ public class AppProviderTest extends FDroidProviderTest {
localized.put("es", es);
localized.put("fr", fr);
Locale.setDefault(new Locale("nl", "NL"));
App.systemLocaleList = LocaleListCompat.forLanguageTags("nl-NL");
app.setLocalized(localized);
assertFalse(app.isLocalized);
@@ -324,7 +325,7 @@ public class AppProviderTest extends FDroidProviderTest {
app.setLocalized(localized);
assertFalse(app.isLocalized);
Locale.setDefault(new Locale("en", "US"));
App.systemLocaleList = LocaleListCompat.forLanguageTags("en-US");
app = new App();
localized.clear();
localized.put("en-US", en);

View File

@@ -1,23 +1,27 @@
package org.fdroid.fdroid.data;
import android.content.res.Configuration;
import android.os.Build;
import android.os.LocaleList;
import androidx.core.os.LocaleListCompat;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.io.FileUtils;
import org.fdroid.fdroid.TestUtils;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.shadows.ShadowLog;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
@RunWith(RobolectricTestRunner.class)
@SuppressWarnings("LocalVariableName")
@@ -25,83 +29,25 @@ public class LocaleSelectionTest {
private static final String KEY = "summary";
@Test
public void correctLocaleSelectionBeforeSDK24() throws Exception {
TestUtils.setFinalStatic(Build.VERSION.class.getDeclaredField("SDK_INT"), 19);
assertTrue(Build.VERSION.SDK_INT < 24);
App app;
private static final String EN_US_NAME = "Checkey: info on local apps\n";
private static final String EN_US_FEATURE_GRAPHIC = "en-US/featureGraphic.png";
private static final String EN_US_PHONE_SCREENSHOT = "en-US/phoneScreenshots/First.png";
private static final String EN_US_SEVEN_INCH_SCREENSHOT = "en-US/sevenInchScreenshots/checkey-tablet.png";
private static final String FR_FR_NAME = "Checkey : infos applis locales";
private static final String FR_CA_FEATURE_GRAPHIC = "fr-CA/featureGraphic.png";
private static final String FR_FR_FEATURE_GRAPHIC = "fr-FR/featureGraphic.png";
private static final String FR_FR_SEVEN_INCH_SCREENSHOT = "fr-FR/sevenInchScreenshots/checkey-tablet.png";
Map<String, Map<String, Object>> localized = new HashMap<>();
HashMap<String, Object> en_US = new HashMap<>();
en_US.put(KEY, "summary-en_US");
HashMap<String, Object> de_AT = new HashMap<>();
de_AT.put(KEY, "summary-de_AT");
HashMap<String, Object> de_DE = new HashMap<>();
de_DE.put(KEY, "summary-de_DE");
HashMap<String, Object> sv = new HashMap<>();
sv.put(KEY, "summary-sv");
HashMap<String, Object> sv_FI = new HashMap<>();
sv_FI.put(KEY, "summary-sv_FI");
localized.put("de-AT", de_AT);
localized.put("de-DE", de_DE);
localized.put("en-US", en_US);
localized.put("sv", sv);
localized.put("sv-FI", sv_FI);
// Easy mode. en-US metadata with an en-US locale
Locale.setDefault(new Locale("en", "US"));
app = new App();
app.setLocalized(localized);
assertEquals(en_US.get(KEY), app.summary);
// Fall back to en-US locale, when we have a different en locale
Locale.setDefault(new Locale("en", "UK"));
app = new App();
app.setLocalized(localized);
assertEquals(en_US.get(KEY), app.summary);
// Fall back to language only
Locale.setDefault(new Locale("en", "UK"));
app = new App();
app.setLocalized(localized);
assertEquals(en_US.get(KEY), app.summary);
// select the correct one out of multiple language locales
Locale.setDefault(new Locale("de", "DE"));
app = new App();
app.setLocalized(localized);
assertEquals(de_DE.get(KEY), app.summary);
// Even when we have a non-exact matching locale, we should fall back to the same language
Locale.setDefault(new Locale("de", "CH"));
app = new App();
app.setLocalized(localized);
assertEquals(de_AT.get(KEY), app.summary);
// Test fallback to base lang with not exact matching locale
Locale.setDefault(new Locale("sv", "SE"));
app = new App();
app.setLocalized(localized);
assertEquals(sv.get(KEY), app.summary);
@Before
public final void setUp() {
ShadowLog.stream = System.out;
}
@Test
public void correctLocaleSelectionFromSDK24() throws Exception {
public void localeSelection() throws Exception {
TestUtils.setFinalStatic(Build.VERSION.class.getDeclaredField("SDK_INT"), 29);
assertTrue(Build.VERSION.SDK_INT >= 24);
App app = spy(new App());
// we mock both the getLocales call and the conversion to a language tag string.
Configuration configuration = mock(Configuration.class);
LocaleList localeList = mock(LocaleList.class);
doReturn(localeList).when(configuration).getLocales();
// Set both default locale as well as the locale list, because the algorithm uses both...
Locale.setDefault(new Locale("en", "US"));
when(localeList.toLanguageTags()).thenReturn("en-US,de-DE");
App app = new App();
App.systemLocaleList = LocaleListCompat.forLanguageTags("en-US,de-DE");
//no metadata present
Map<String, Map<String, Object>> localized = new HashMap<>();
@@ -112,10 +58,18 @@ public class LocaleSelectionTest {
en_US.put(KEY, "summary-en_US");
HashMap<String, Object> en_GB = new HashMap<>();
en_GB.put(KEY, "summary-en_GB");
HashMap<String, Object> de = new HashMap<>();
de.put(KEY, "summary-de");
HashMap<String, Object> de_AT = new HashMap<>();
de_AT.put(KEY, "summary-de_AT");
HashMap<String, Object> de_DE = new HashMap<>();
de_DE.put(KEY, "summary-de_DE");
HashMap<String, Object> es_ES = new HashMap<>();
es_ES.put(KEY, "summary-es_ES");
HashMap<String, Object> fr_FR = new HashMap<>();
fr_FR.put(KEY, "summary-fr_FR");
HashMap<String, Object> it_IT = new HashMap<>();
it_IT.put(KEY, "summary-it_IT");
app.summary = "reset";
localized.put("de-AT", de_AT);
@@ -125,8 +79,7 @@ public class LocaleSelectionTest {
// just select the matching en-US locale, nothing special here
assertEquals(en_US.get(KEY), app.summary);
Locale.setDefault(new Locale("en", "SE"));
when(localeList.toLanguageTags()).thenReturn("en-SE,de-DE");
App.systemLocaleList = LocaleListCompat.forLanguageTags("en-SE,de-DE");
app.setLocalized(localized);
// Fall back to another en locale before de
assertEquals(en_US.get(KEY), app.summary);
@@ -138,8 +91,7 @@ public class LocaleSelectionTest {
localized.put("en-GB", en_GB);
localized.put("en-US", en_US);
Locale.setDefault(new Locale("de", "AT"));
when(localeList.toLanguageTags()).thenReturn("de-AT,de-DE");
App.systemLocaleList = LocaleListCompat.forLanguageTags("de-AT,de-DE");
app.setLocalized(localized);
// full match against a non-default locale
assertEquals(de_AT.get(KEY), app.summary);
@@ -147,14 +99,13 @@ public class LocaleSelectionTest {
app.summary = "reset";
localized.clear();
localized.put("de-AT", de_AT);
localized.put("de", de_DE);
localized.put("de", de);
localized.put("en-GB", en_GB);
localized.put("en-US", en_US);
Locale.setDefault(new Locale("de", "CH"));
when(localeList.toLanguageTags()).thenReturn("de-CH,en-US");
App.systemLocaleList = LocaleListCompat.forLanguageTags("de-CH,en-US");
app.setLocalized(localized);
assertEquals(de_DE.get(KEY), app.summary);
assertEquals(de.get(KEY), app.summary);
app.summary = "reset";
localized.clear();
@@ -162,13 +113,12 @@ public class LocaleSelectionTest {
localized.put("en-US", en_US);
Locale.setDefault(new Locale("en", "AU"));
when(localeList.toLanguageTags()).thenReturn("en-AU");
App.systemLocaleList = LocaleListCompat.forLanguageTags("en-AU");
app.setLocalized(localized);
assertEquals(en_US.get(KEY), app.summary);
app.summary = "reset";
Locale.setDefault(new Locale("zh", "TW", "#Hant"));
when(localeList.toLanguageTags()).thenReturn("zh-Hant-TW,zh-Hans-CN");
App.systemLocaleList = LocaleListCompat.forLanguageTags("zh-Hant-TW,zh-Hans-CN");
localized.clear();
localized.put("en", en_GB);
localized.put("en-US", en_US);
@@ -197,5 +147,107 @@ public class LocaleSelectionTest {
localized.put("zh-CN", zh_CN);
app.setLocalized(localized);
assertEquals(zh_CN.get(KEY), app.summary);
localized.clear();
localized.put("en-US", en_US);
localized.put("zh-CN", zh_CN);
app.setLocalized(localized);
assertEquals(zh_CN.get(KEY), app.summary);
// https://developer.android.com/guide/topics/resources/multilingual-support#resource-resolution-examples
App.systemLocaleList = LocaleListCompat.forLanguageTags("fr-CH");
localized.clear();
localized.put("en-US", en_US);
localized.put("de-DE", de_DE);
localized.put("es-ES", es_ES);
localized.put("fr-FR", fr_FR);
localized.put("it-IT", it_IT);
app.setLocalized(localized);
assertEquals(fr_FR.get(KEY), app.summary);
// https://developer.android.com/guide/topics/resources/multilingual-support#t-2d-choice
App.systemLocaleList = LocaleListCompat.forLanguageTags("fr-CH,it-CH");
localized.clear();
localized.put("en-US", en_US);
localized.put("de-DE", de_DE);
localized.put("es-ES", es_ES);
localized.put("it-IT", it_IT);
app.setLocalized(localized);
assertEquals(it_IT.get(KEY), app.summary);
}
@Test
public void testSetLocalized() throws IOException {
Assume.assumeTrue(Build.VERSION.SDK_INT >= 24);
File f = TestUtils.copyResourceToTempFile("localized.json");
Map<String, Object> result = new ObjectMapper().readValue(
FileUtils.readFileToString(f, (String) null), HashMap.class);
List<Map<String, Object>> apps = (List<Map<String, Object>>) result.get("apps");
Map<String, Map<String, Object>> localized = (Map<String, Map<String, Object>>) apps.get(0).get("localized");
App app = new App();
App.systemLocaleList = LocaleListCompat.create(Locale.US);
app.setLocalized(localized);
assertEquals(EN_US_NAME, app.name);
assertEquals(EN_US_FEATURE_GRAPHIC, app.featureGraphic);
assertEquals(EN_US_PHONE_SCREENSHOT, app.phoneScreenshots[0]);
assertEquals(EN_US_SEVEN_INCH_SCREENSHOT, app.sevenInchScreenshots[0]);
assertTrue(app.isLocalized);
// choose the language when there is an exact locale match
App.systemLocaleList = LocaleListCompat.forLanguageTags("fr-FR");
app.setLocalized(localized);
assertEquals(FR_FR_NAME, app.name);
assertEquals(FR_FR_FEATURE_GRAPHIC, app.featureGraphic);
assertEquals(EN_US_PHONE_SCREENSHOT, app.phoneScreenshots[0]);
assertEquals(FR_FR_SEVEN_INCH_SCREENSHOT, app.sevenInchScreenshots[0]);
assertTrue(app.isLocalized);
// choose the language from a different country when the preferred country is not available,
// while still choosing featureGraphic from exact match
App.systemLocaleList = LocaleListCompat.create(Locale.CANADA_FRENCH);
app.setLocalized(localized);
assertEquals(FR_FR_NAME, app.name);
assertEquals(FR_CA_FEATURE_GRAPHIC, app.featureGraphic);
assertEquals(EN_US_PHONE_SCREENSHOT, app.phoneScreenshots[0]);
assertEquals(FR_FR_SEVEN_INCH_SCREENSHOT, app.sevenInchScreenshots[0]);
assertTrue(app.isLocalized);
// choose the third preferred language when first and second lack translations
App.systemLocaleList = LocaleListCompat.forLanguageTags("bo-IN,sr-RS,fr-FR");
app.setLocalized(localized);
assertEquals(FR_FR_NAME, app.name);
assertEquals(FR_FR_FEATURE_GRAPHIC, app.featureGraphic);
assertEquals(EN_US_PHONE_SCREENSHOT, app.phoneScreenshots[0]);
assertEquals(FR_FR_SEVEN_INCH_SCREENSHOT, app.sevenInchScreenshots[0]);
assertTrue(app.isLocalized);
// choose first language from different country, rather than 2nd full lang/country match
App.systemLocaleList = LocaleListCompat.forLanguageTags("en-GB,fr-FR");
app.setLocalized(localized);
assertEquals(EN_US_NAME, app.name);
assertEquals(EN_US_FEATURE_GRAPHIC, app.featureGraphic);
assertEquals(EN_US_PHONE_SCREENSHOT, app.phoneScreenshots[0]);
assertEquals(EN_US_SEVEN_INCH_SCREENSHOT, app.sevenInchScreenshots[0]);
assertTrue(app.isLocalized);
// choose en_US when no match, and mark as not localized
App.systemLocaleList = LocaleListCompat.forLanguageTags("bo-IN,sr-RS");
app.setLocalized(localized);
assertEquals(EN_US_NAME, app.name);
assertEquals(EN_US_FEATURE_GRAPHIC, app.featureGraphic);
assertEquals(EN_US_PHONE_SCREENSHOT, app.phoneScreenshots[0]);
assertEquals(EN_US_SEVEN_INCH_SCREENSHOT, app.sevenInchScreenshots[0]);
assertFalse(app.isLocalized);
// When English is the preferred language and the second language has no entries
App.systemLocaleList = LocaleListCompat.forLanguageTags("en-US,sr-RS");
app.setLocalized(localized);
assertEquals(EN_US_NAME, app.name);
assertEquals(EN_US_FEATURE_GRAPHIC, app.featureGraphic);
assertEquals(EN_US_PHONE_SCREENSHOT, app.phoneScreenshots[0]);
assertEquals(EN_US_SEVEN_INCH_SCREENSHOT, app.sevenInchScreenshots[0]);
assertTrue(app.isLocalized);
}
}

View File

@@ -370,6 +370,7 @@ public class IndexV1UpdaterTest extends FDroidProviderTest {
"isLocalized",
"preferredSigner",
"prefs",
"systemLocaleList",
"TAG",
};
runJsonIgnoreTest(new App(), allowedInApp, ignoredInApp);