diff --git a/app/src/main/java/org/fdroid/fdroid/data/App.java b/app/src/main/java/org/fdroid/fdroid/data/App.java index 386623b67..25f1fcd2b 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/App.java +++ b/app/src/main/java/org/fdroid/fdroid/data/App.java @@ -19,6 +19,7 @@ import android.text.TextUtils; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; @@ -534,8 +535,17 @@ public class App extends ValueObject implements Comparable, Parcelable { if (availableLocales.contains(languageTag)) { localesToUse.add(languageTag); } + if (localesToUse.isEmpty()) { + // In case of non-standard region like [en-SE] + for (String availableLocale : availableLocales) { + String availableLanguage = availableLocale.split("-")[0]; + if (languageTag.equals(availableLanguage)) { + localesToUse.add(availableLocale); + } + } + } if (Build.VERSION.SDK_INT >= 24) { - LocaleList localeList = Resources.getSystem().getConfiguration().getLocales(); + LocaleList localeList = getLocales(); String[] sortedLocaleList = localeList.toLanguageTags().split(","); Arrays.sort(sortedLocaleList, new java.util.Comparator() { @Override @@ -615,6 +625,11 @@ public class App extends ValueObject implements Comparable, Parcelable { tvScreenshots = getLocalizedListEntry(localized, localesToUse, "tvScreenshots"); } + @RequiresApi(api = Build.VERSION_CODES.N) + LocaleList getLocales() { + return Resources.getSystem().getConfiguration().getLocales(); + } + /** * Returns the right localized version of this entry, based on an immitation of * the logic that Android/Java uses. On Android >= 24, this can get the diff --git a/app/src/test/java/org/fdroid/fdroid/TestUtils.java b/app/src/test/java/org/fdroid/fdroid/TestUtils.java index 3c860831c..ef5a46804 100644 --- a/app/src/test/java/org/fdroid/fdroid/TestUtils.java +++ b/app/src/test/java/org/fdroid/fdroid/TestUtils.java @@ -26,6 +26,8 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.security.NoSuchAlgorithmException; import static org.junit.Assert.assertEquals; @@ -177,4 +179,15 @@ public class TestUtils { AppProvider.Helper.calcSuggestedApks(context); AppProvider.Helper.recalculatePreferredMetadata(context); } + + /** + * Set a static final field through reflection + */ + public static void setFinalStatic(Field field, Object newValue) throws Exception { + field.setAccessible(true); + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); + field.set(null, newValue); + } } diff --git a/app/src/test/java/org/fdroid/fdroid/data/LocaleSelectionTest.java b/app/src/test/java/org/fdroid/fdroid/data/LocaleSelectionTest.java new file mode 100644 index 000000000..60ec8b872 --- /dev/null +++ b/app/src/test/java/org/fdroid/fdroid/data/LocaleSelectionTest.java @@ -0,0 +1,198 @@ +package org.fdroid.fdroid.data; + + +import android.os.Build; +import android.os.LocaleList; +import org.fdroid.fdroid.TestUtils; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +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) +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; + + Map> localized = new HashMap<>(); + HashMap en_US = new HashMap<>(); + en_US.put(KEY, "summary-en_US"); + HashMap de_AT = new HashMap<>(); + de_AT.put(KEY, "summary-de_AT"); + HashMap de_DE = new HashMap<>(); + de_DE.put(KEY, "summary-de_DE"); + HashMap sv = new HashMap<>(); + sv.put(KEY, "summary-sv"); + HashMap 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); + } + + @Test + public void correctLocaleSelectionFromSDK24() throws Exception { + + TestUtils.setFinalStatic(Build.VERSION.class.getDeclaredField("SDK_INT"), 29); + assertTrue(Build.VERSION.SDK_INT >= 24); + + App app = spy(new App()); + LocaleList localeList = mock(LocaleList.class); + + // we mock both the getLocales call and the conversion to a language tag string. + doReturn(localeList).when(app).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"); + + //no metadata present + Map> localized = new HashMap<>(); + app.setLocalized(localized); + assertEquals("Unknown application", app.summary); + + HashMap en_US = new HashMap<>(); + en_US.put(KEY, "summary-en_US"); + HashMap en_GB = new HashMap<>(); + en_GB.put(KEY, "summary-en_GB"); + HashMap de_AT = new HashMap<>(); + de_AT.put(KEY, "summary-de_AT"); + HashMap de_DE = new HashMap<>(); + de_DE.put(KEY, "summary-de_DE"); + + app.summary = "reset"; + localized.put("de-AT", de_AT); + localized.put("de-DE", de_DE); + localized.put("en-US", en_US); + app.setLocalized(localized); + // 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.setLocalized(localized); + // Fall back to another en locale before de + assertEquals(en_US.get(KEY), app.summary); + + app.summary = "reset"; + localized.clear(); + localized.put("de-AT", de_AT); + localized.put("de-DE", de_DE); + 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.setLocalized(localized); + // full match against a non-default locale + assertEquals(de_AT.get(KEY), app.summary); + + app.summary = "reset"; + localized.clear(); + localized.put("de-AT", de_AT); + localized.put("de", 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.setLocalized(localized); + assertEquals(de_DE.get(KEY), app.summary); + + app.summary = "reset"; + localized.clear(); + localized.put("en-GB", en_GB); + localized.put("en-US", en_US); + + Locale.setDefault(new Locale("en", "AU")); + when(localeList.toLanguageTags()).thenReturn("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"); + localized.clear(); + localized.put("en", en_GB); + localized.put("en-US", en_US); + app.setLocalized(localized); + //No match at all, fall back to an english locale + assertEquals(en_US.get(KEY), app.summary); + + app.summary = "reset"; + HashMap zh_TW = new HashMap<>(); + zh_TW.put(KEY, "summary-zh_TW"); + HashMap zh_CN = new HashMap<>(); + zh_CN.put(KEY, "summary-zh_CN"); + HashMap zh_HK = new HashMap<>(); + zh_HK.put(KEY, "summary-zh_HK"); + + localized.clear(); + localized.put("en-US", en_US); + localized.put("zh-CN", zh_CN); + localized.put("zh-HK", zh_HK); + localized.put("zh-TW", zh_TW); + app.setLocalized(localized); + assertEquals(zh_TW.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); + } +}