Merge branch 'latest-tab-localized-overhaul' into 'master'
convert the Latest Tab SELECT logic to ORDER BY, with accurate IS_LOCALIZED Closes #939, #2024, #1186, and #987 See merge request fdroid/fdroidclient!971
This commit is contained in:
commit
b9efb143be
@ -26,14 +26,16 @@ track of modifications and fuzzy translations. Applying translations manually
|
|||||||
skips all of the fixes and checks, and overrides the fuzzy state of strings.
|
skips all of the fixes and checks, and overrides the fuzzy state of strings.
|
||||||
|
|
||||||
Note that you cannot change the English strings on Weblate. If you have any
|
Note that you cannot change the English strings on Weblate. If you have any
|
||||||
suggestions on how to improve them, open a merge request like you would if you
|
suggestions on how to improve them, open an issue or merge request like you
|
||||||
were making code changes. This way the changes can be reviewed before the
|
would if you were making code changes. This way the changes can be reviewed
|
||||||
source strings on Weblate are changed.
|
before the source strings on Weblate are changed.
|
||||||
|
|
||||||
|
|
||||||
## Code Style
|
## Code Style
|
||||||
|
|
||||||
We follow the [Android Java style](https://source.android.com/source/code-style.html).
|
We follow the default Android Studio code formatter (e.g. `Ctrl-Alt-L`). This
|
||||||
Some key points:
|
should be more or less the same as [Android Java
|
||||||
|
style](https://source.android.com/source/code-style.html). Some key points:
|
||||||
|
|
||||||
* Four space indentation
|
* Four space indentation
|
||||||
* UTF-8 source files
|
* UTF-8 source files
|
||||||
@ -45,76 +47,35 @@ Some key points:
|
|||||||
* Braces are always used after if, for and while
|
* Braces are always used after if, for and while
|
||||||
|
|
||||||
The current code base doesn't follow it entirely, but new code should follow
|
The current code base doesn't follow it entirely, but new code should follow
|
||||||
it. We enforce some of these, but not all, via checkstyle.
|
it. We enforce some of these, but not all, via `./gradlew checkstyle`.
|
||||||
|
|
||||||
## Debugging
|
|
||||||
|
|
||||||
To get all the logcat messages by F-Droid, you can run:
|
|
||||||
|
|
||||||
adb logcat | grep `adb shell ps | grep org.fdroid.fdroid | cut -c10-15`
|
|
||||||
|
|
||||||
## Building tips
|
|
||||||
|
|
||||||
* Use gradle with `--daemon` if you are going to build F-Droid multiple times.
|
|
||||||
* If you get a message like `Could not find com.android.support:support-...`,
|
|
||||||
make sure that you have the latest Android support maven repository.
|
|
||||||
* When building as part of AOSP with `Android.mk`, make sure you have a
|
|
||||||
recent version of Gradle installed as `gradlew` will not be used.
|
|
||||||
|
|
||||||
## Running the test suite
|
## Running the test suite
|
||||||
|
|
||||||
|
Before pushing commits to a merge request, make sure this passes:
|
||||||
|
|
||||||
|
./gradlew checkstyle pmd lint
|
||||||
|
|
||||||
In order to run the F-Droid test suite, you will need to have either a real device
|
In order to run the F-Droid test suite, you will need to have either a real device
|
||||||
connected via `adb`, or an emulator running. Then, execute the following from the
|
connected via `adb`, or an emulator running. Then, execute the following from the
|
||||||
command line:
|
command line:
|
||||||
|
|
||||||
./gradlew check
|
./gradlew check
|
||||||
|
|
||||||
Note that the CI already runs the tests on an emulator, so you don't
|
Many important tests require a device or emulator, but do not work in GitLab CI.
|
||||||
necessarily have to do this yourself if you open a merge request as the tests
|
That mean they need to be run locally, and that is usually easiest in Android
|
||||||
will get run.
|
Studio rather than the command line.
|
||||||
|
|
||||||
### Running tests in Android Studio
|
For a quick way to run a specific JUnit/Robolectric test:
|
||||||
|
|
||||||
Later versions of Android Studio require tests to be run with a "Working directory"
|
./gradlew testFullDebugUnitTest --tests *LocaleSelectionTest*
|
||||||
of `$MODULE_DIR$`.
|
|
||||||
[To make this the default behaviour](https://code.google.com/p/android/issues/detail?id=158015#c11),
|
|
||||||
close any projects to get the Welcome dialog. Then choose _Configure > Project Defaults >
|
|
||||||
Run Configurations > Defaults > Android JUnit_, and change "Working directory"
|
|
||||||
to `$MODULE_DIR$`. If you already have a project setup in Android Studio, you
|
|
||||||
may also need to change the default in _Run > Edit Configurations... > Defaults >
|
|
||||||
Android JUnit_.
|
|
||||||
|
|
||||||
## Versioning
|
For a quick way to run a specific emulator test:
|
||||||
|
|
||||||
Each stable version follows the `X.Y` pattern. Hotfix releases - i.e. when a
|
./gradlew connectedFullDebugAndroidTest \
|
||||||
stable has an important bug that needs immediate fixing - will follow the
|
-Pandroid.testInstrumentationRunnerArguments.class=org.fdroid.fdroid.MainActivityExpressoTest
|
||||||
`X.Y.Z` pattern.
|
|
||||||
|
|
||||||
Before each stable release, a number of alpha releases will be released. They
|
|
||||||
will follow the pattern `X.Y-alphaN`, where `N` is the current alpha number.
|
|
||||||
These will usually include changes and new features that have not been tested
|
|
||||||
enough for a stable release, so use at your own risk. Testers and reporters
|
|
||||||
are very welcome.
|
|
||||||
|
|
||||||
The version codes use a number of digits per each of these keys: `XXXYYYZNN`.
|
## Making releases
|
||||||
So for example, 1.3.1 would be `1003150` and 0.95-alpha13 would be `95013`
|
|
||||||
(leading zeros are omitted).
|
|
||||||
|
|
||||||
Note that we use a trailing `50` for actual stable releases, so alphas are
|
See https://gitlab.com/fdroid/wiki/-/wikis/Internal/Release-Process#fdroidclient
|
||||||
limited to `-alpha49`.
|
|
||||||
|
|
||||||
This is an example of a release process for **0.95**:
|
|
||||||
|
|
||||||
* We are currently at stable **0.94**
|
|
||||||
* **0.95-alpha1** is released
|
|
||||||
* **0.95-alpha2** is released
|
|
||||||
* **0.95-alpha3** is released
|
|
||||||
* `stable-v0.95` is branched and frozen
|
|
||||||
* **0.95** is released
|
|
||||||
* A bug is reported on the stable release and fixed
|
|
||||||
* **0.95.1** is released with only that fix
|
|
||||||
|
|
||||||
As soon as a stable is tagged, master will move on to `-alpha0` on the next
|
|
||||||
version. This is a temporary measure - until `-alpha1` is released - so that
|
|
||||||
moving from stable to master doesn't require a downgrade. `-alpha0` versions
|
|
||||||
will not be tagged nor released.
|
|
||||||
|
@ -67,6 +67,7 @@ import org.apache.commons.net.util.SubnetUtils;
|
|||||||
import org.fdroid.fdroid.Preferences.ChangeListener;
|
import org.fdroid.fdroid.Preferences.ChangeListener;
|
||||||
import org.fdroid.fdroid.Preferences.Theme;
|
import org.fdroid.fdroid.Preferences.Theme;
|
||||||
import org.fdroid.fdroid.compat.PRNGFixes;
|
import org.fdroid.fdroid.compat.PRNGFixes;
|
||||||
|
import org.fdroid.fdroid.data.App;
|
||||||
import org.fdroid.fdroid.data.AppProvider;
|
import org.fdroid.fdroid.data.AppProvider;
|
||||||
import org.fdroid.fdroid.data.InstalledAppProviderService;
|
import org.fdroid.fdroid.data.InstalledAppProviderService;
|
||||||
import org.fdroid.fdroid.data.Repo;
|
import org.fdroid.fdroid.data.Repo;
|
||||||
@ -330,6 +331,7 @@ public class FDroidApp extends Application {
|
|||||||
public void onConfigurationChanged(Configuration newConfig) {
|
public void onConfigurationChanged(Configuration newConfig) {
|
||||||
super.onConfigurationChanged(newConfig);
|
super.onConfigurationChanged(newConfig);
|
||||||
Languages.setLanguage(this);
|
Languages.setLanguage(this);
|
||||||
|
App.systemLocaleList = null;
|
||||||
|
|
||||||
// update the descriptions based on the new language preferences
|
// update the descriptions based on the new language preferences
|
||||||
SharedPreferences atStartTime = getAtStartTimeSharedPreferences();
|
SharedPreferences atStartTime = getAtStartTimeSharedPreferences();
|
||||||
|
@ -19,6 +19,8 @@ import android.text.TextUtils;
|
|||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.os.ConfigurationCompat;
|
||||||
|
import androidx.core.os.LocaleListCompat;
|
||||||
import com.fasterxml.jackson.annotation.JacksonInject;
|
import com.fasterxml.jackson.annotation.JacksonInject;
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
@ -41,8 +43,8 @@ import java.util.Arrays;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -75,6 +77,14 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
|
|||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
private static final String TAG = "App";
|
private static final String TAG = "App";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link LocaleListCompat} for finding the right app description material.
|
||||||
|
* It is set globally static to a) cache this value, since there are thousands
|
||||||
|
* of {@link App} entries, and b) make it easy to test {@link #setLocalized(Map)} )}
|
||||||
|
*/
|
||||||
|
@JsonIgnore
|
||||||
|
public static LocaleListCompat systemLocaleList;
|
||||||
|
|
||||||
// these properties are not from the index metadata, but represent the state on the device
|
// these properties are not from the index metadata, but represent the state on the device
|
||||||
/**
|
/**
|
||||||
* True if compatible with the device (i.e. if at least one apk is)
|
* True if compatible with the device (i.e. if at least one apk is)
|
||||||
@ -98,8 +108,12 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
|
|||||||
public String preferredSigner;
|
public String preferredSigner;
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
public boolean isApk;
|
public boolean isApk;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Has this {@code App} been localized into one of the user's current locales.
|
||||||
|
*/
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
boolean isLocalized = false;
|
boolean isLocalized;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is primarily for the purpose of saving app metadata when parsing an index.xml file.
|
* This is primarily for the purpose of saving app metadata when parsing an index.xml file.
|
||||||
@ -484,199 +498,115 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
|
|||||||
* {@code localized} block is included in the index. Also, null strings in
|
* {@code localized} block is included in the index. Also, null strings in
|
||||||
* the {@code localized} block should not overwrite Name/Summary/Description
|
* the {@code localized} block should not overwrite Name/Summary/Description
|
||||||
* strings with empty/null if they were set directly by Jackson.
|
* strings with empty/null if they were set directly by Jackson.
|
||||||
* <p>
|
* <ol>
|
||||||
* Choosing the locale to use follows two sets of rules, one for Android versions
|
|
||||||
* older than {@code android-24} and the other for {@code android-24} or newer.
|
|
||||||
* The system-wide language preference list was added in {@code android-24}.
|
|
||||||
* <ul>
|
|
||||||
* <li>{@code >= android-24}<ol>
|
|
||||||
* <li>the country variant {@code de-AT} from the user locale list
|
* <li>the country variant {@code de-AT} from the user locale list
|
||||||
* <li>only the language {@code de} from the above locale
|
* <li>only the language {@code de} from the above locale
|
||||||
|
* <li>next locale in the user's preference list ({@code >= android-24})
|
||||||
* <li>{@code en-US} since its the most common English for software
|
* <li>{@code en-US} since its the most common English for software
|
||||||
* <li>the first available {@code en} locale
|
* <li>the first available {@code en} locale
|
||||||
* </ol></li>
|
* </ol>
|
||||||
* <li>{@code < android-24}<ol>
|
|
||||||
* <li>the country variant from the user locale: {@code de-AT}
|
|
||||||
* <li>only the language from the above locale: {@code de}
|
|
||||||
* <li>all available locales with the same language: {@code de-BE}
|
|
||||||
* <li>{@code en-US} since its the most common English for software
|
|
||||||
* <li>all available {@code en} locales
|
|
||||||
* </ol></li>
|
|
||||||
* </ul>
|
|
||||||
* On {@code >= android-24}, it is by design that this does not fallback to other
|
|
||||||
* country-specific locales, e.g. {@code fr-CH} does not fall back on {@code fr-FR}.
|
|
||||||
* If someone wants to fallback to {@code fr-FR}, they can add it to the system
|
|
||||||
* language list. There are many cases where it is inappropriate to fallback to a
|
|
||||||
* different country-specific locale, for example {@code de-DE --> de-CH} or
|
|
||||||
* {@code zh-CN --> zh-TW}.
|
|
||||||
* <p>
|
* <p>
|
||||||
* On {@code < android-24}, the user can only set a single
|
* The system-wide language preference list was added in {@code android-24}.
|
||||||
* locale with a country as an option, so here it makes sense to try to fallback
|
*
|
||||||
* on other country-specific locales, rather than English.
|
* @see <a href="https://developer.android.com/guide/topics/resources/multilingual-support">Android language and locale resolution overview</a>
|
||||||
*/
|
*/
|
||||||
@JsonProperty("localized")
|
@JsonProperty("localized")
|
||||||
void setLocalized(Map<String, Map<String, Object>> localized) { // NOPMD
|
void setLocalized(Map<String, Map<String, Object>> localized) { // NOPMD
|
||||||
Locale defaultLocale = Locale.getDefault();
|
if (systemLocaleList == null) {
|
||||||
String languageTag = defaultLocale.getLanguage();
|
systemLocaleList = ConfigurationCompat.getLocales(Resources.getSystem().getConfiguration());
|
||||||
String countryTag = defaultLocale.getCountry();
|
|
||||||
String localeTag;
|
|
||||||
if (TextUtils.isEmpty(countryTag)) {
|
|
||||||
localeTag = languageTag;
|
|
||||||
} else {
|
|
||||||
localeTag = languageTag + "-" + countryTag;
|
|
||||||
}
|
}
|
||||||
|
Set<String> supportedLocales = localized.keySet();
|
||||||
Set<String> availableLocales = localized.keySet();
|
setIsLocalized(supportedLocales);
|
||||||
Set<String> localesToUse = new LinkedHashSet<>();
|
String value = getLocalizedEntry(localized, supportedLocales, "whatsNew");
|
||||||
if (availableLocales.contains(localeTag)) {
|
|
||||||
localesToUse.add(localeTag);
|
|
||||||
}
|
|
||||||
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();
|
|
||||||
String[] sortedLocaleList = localeList.toLanguageTags().split(",");
|
|
||||||
Arrays.sort(sortedLocaleList, new java.util.Comparator<String>() {
|
|
||||||
@Override
|
|
||||||
public int compare(String s1, String s2) {
|
|
||||||
return s1.length() - s2.length();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
for (String toUse : sortedLocaleList) {
|
|
||||||
localesToUse.add(toUse);
|
|
||||||
for (String l : availableLocales) {
|
|
||||||
if (l.equals(toUse.split("-")[0])) {
|
|
||||||
localesToUse.add(l);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (String l : availableLocales) {
|
|
||||||
if (l.startsWith(languageTag)) {
|
|
||||||
localesToUse.add(l);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (availableLocales.contains("en-US")) {
|
|
||||||
localesToUse.add("en-US");
|
|
||||||
}
|
|
||||||
for (String l : availableLocales) {
|
|
||||||
if (l.startsWith("en")) {
|
|
||||||
localesToUse.add(l);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String l : localesToUse) {
|
|
||||||
if (l.startsWith(languageTag)) {
|
|
||||||
isLocalized = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (localesToUse.size() > 1) {
|
|
||||||
isLocalized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
String value = getLocalizedEntry(localized, localesToUse, "whatsNew");
|
|
||||||
if (!TextUtils.isEmpty(value)) {
|
if (!TextUtils.isEmpty(value)) {
|
||||||
whatsNew = value;
|
whatsNew = value;
|
||||||
}
|
}
|
||||||
value = getLocalizedEntry(localized, localesToUse, "video");
|
|
||||||
|
value = getLocalizedEntry(localized, supportedLocales, "video");
|
||||||
if (!TextUtils.isEmpty(value)) {
|
if (!TextUtils.isEmpty(value)) {
|
||||||
video = value.split("\n", 1)[0];
|
video = value.trim();
|
||||||
}
|
}
|
||||||
value = getLocalizedEntry(localized, localesToUse, "name");
|
value = getLocalizedEntry(localized, supportedLocales, "name");
|
||||||
if (!TextUtils.isEmpty(value)) {
|
if (!TextUtils.isEmpty(value)) {
|
||||||
name = value;
|
name = value.trim();
|
||||||
}
|
}
|
||||||
value = getLocalizedEntry(localized, localesToUse, "summary");
|
value = getLocalizedEntry(localized, supportedLocales, "summary");
|
||||||
if (!TextUtils.isEmpty(value)) {
|
if (!TextUtils.isEmpty(value)) {
|
||||||
summary = value;
|
summary = value.trim();
|
||||||
}
|
}
|
||||||
value = getLocalizedEntry(localized, localesToUse, "description");
|
value = getLocalizedEntry(localized, supportedLocales, "description");
|
||||||
if (!TextUtils.isEmpty(value)) {
|
if (!TextUtils.isEmpty(value)) {
|
||||||
description = formatDescription(value);
|
description = formatDescription(value);
|
||||||
}
|
}
|
||||||
value = getLocalizedGraphicsEntry(localized, localesToUse, "icon");
|
value = getLocalizedGraphicsEntry(localized, supportedLocales, "icon");
|
||||||
if (!TextUtils.isEmpty(value)) {
|
if (!TextUtils.isEmpty(value)) {
|
||||||
iconUrl = value;
|
iconUrl = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
featureGraphic = getLocalizedGraphicsEntry(localized, localesToUse, "featureGraphic");
|
featureGraphic = getLocalizedGraphicsEntry(localized, supportedLocales, "featureGraphic");
|
||||||
promoGraphic = getLocalizedGraphicsEntry(localized, localesToUse, "promoGraphic");
|
promoGraphic = getLocalizedGraphicsEntry(localized, supportedLocales, "promoGraphic");
|
||||||
tvBanner = getLocalizedGraphicsEntry(localized, localesToUse, "tvBanner");
|
tvBanner = getLocalizedGraphicsEntry(localized, supportedLocales, "tvBanner");
|
||||||
|
|
||||||
wearScreenshots = getLocalizedListEntry(localized, localesToUse, "wearScreenshots");
|
wearScreenshots = getLocalizedListEntry(localized, supportedLocales, "wearScreenshots");
|
||||||
phoneScreenshots = getLocalizedListEntry(localized, localesToUse, "phoneScreenshots");
|
phoneScreenshots = getLocalizedListEntry(localized, supportedLocales, "phoneScreenshots");
|
||||||
sevenInchScreenshots = getLocalizedListEntry(localized, localesToUse, "sevenInchScreenshots");
|
sevenInchScreenshots = getLocalizedListEntry(localized, supportedLocales, "sevenInchScreenshots");
|
||||||
tenInchScreenshots = getLocalizedListEntry(localized, localesToUse, "tenInchScreenshots");
|
tenInchScreenshots = getLocalizedListEntry(localized, supportedLocales, "tenInchScreenshots");
|
||||||
tvScreenshots = getLocalizedListEntry(localized, localesToUse, "tvScreenshots");
|
tvScreenshots = getLocalizedListEntry(localized, supportedLocales, "tvScreenshots");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the right localized version of this entry, based on an immitation of
|
* Sets the boolean flag {@link #isLocalized} if this app entry has an localized
|
||||||
* the logic that Android/Java uses. On Android >= 24, this can get the
|
* entry in one of the user's current locales.
|
||||||
* "Language Priority List", but it doesn't always seem to be properly sorted.
|
*
|
||||||
* So this method has to kind of fake it by using {@link Locale#getDefault()}
|
* @see org.fdroid.fdroid.views.main.WhatsNewViewBinder#onCreateLoader(int, android.os.Bundle)
|
||||||
* as the first entry, then sorting the rest based on length (e.g. {@code de-AT}
|
*/
|
||||||
* before {@code de}).
|
private void setIsLocalized(Set<String> supportedLocales) {
|
||||||
|
isLocalized = false;
|
||||||
|
for (int i = 0; i < systemLocaleList.size(); i++) {
|
||||||
|
String language = systemLocaleList.get(i).getLanguage();
|
||||||
|
for (String supportedLocale : supportedLocales) {
|
||||||
|
if (language.equals(supportedLocale.split("-")[0])) {
|
||||||
|
isLocalized = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the right localized version of this entry, based on an imitation of
|
||||||
|
* the logic that Android uses.
|
||||||
*
|
*
|
||||||
* @see LocaleList
|
* @see LocaleList
|
||||||
* @see Locale#getDefault()
|
|
||||||
* @see java.util.Locale.LanguageRange
|
|
||||||
*/
|
*/
|
||||||
private String getLocalizedEntry(Map<String, Map<String, Object>> localized,
|
private String getLocalizedEntry(Map<String, Map<String, Object>> localized,
|
||||||
Set<String> locales, String key) {
|
Set<String> supportedLocales, @NonNull String key) {
|
||||||
try {
|
Map<String, Object> localizedLocaleMap = getLocalizedLocaleMap(localized, supportedLocales, key);
|
||||||
for (String locale : locales) {
|
if (localizedLocaleMap != null && !localizedLocaleMap.isEmpty()) {
|
||||||
if (localized.containsKey(locale)) {
|
for (Object entry : localizedLocaleMap.values()) {
|
||||||
String value = (String) localized.get(locale).get(key);
|
return (String) entry; // NOPMD
|
||||||
if (value != null) {
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (ClassCastException e) {
|
|
||||||
Utils.debugLog(TAG, e.getMessage());
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getLocalizedGraphicsEntry(Map<String, Map<String, Object>> localized,
|
private String getLocalizedGraphicsEntry(Map<String, Map<String, Object>> localized,
|
||||||
Set<String> locales, String key) {
|
Set<String> supportedLocales, @NonNull String key) {
|
||||||
try {
|
Map<String, Object> localizedLocaleMap = getLocalizedLocaleMap(localized, supportedLocales, key);
|
||||||
for (String locale : locales) {
|
if (localizedLocaleMap != null && !localizedLocaleMap.isEmpty()) {
|
||||||
Map<String, Object> entry = localized.get(locale);
|
for (String locale : localizedLocaleMap.keySet()) {
|
||||||
if (entry != null) {
|
return locale + "/" + localizedLocaleMap.get(locale); // NOPMD
|
||||||
Object value = entry.get(key);
|
|
||||||
if (value != null && value.toString().length() > 0) {
|
|
||||||
return locale + "/" + value;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (ClassCastException e) {
|
|
||||||
Utils.debugLog(TAG, e.getMessage());
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String[] getLocalizedListEntry(Map<String, Map<String, Object>> localized,
|
private String[] getLocalizedListEntry(Map<String, Map<String, Object>> localized,
|
||||||
Set<String> locales, String key) {
|
Set<String> supportedLocales, @NonNull String key) {
|
||||||
try {
|
Map<String, Object> localizedLocaleMap = getLocalizedLocaleMap(localized, supportedLocales, key);
|
||||||
for (String locale : locales) {
|
if (localizedLocaleMap != null && !localizedLocaleMap.isEmpty()) {
|
||||||
if (localized.containsKey(locale)) {
|
for (String locale : localizedLocaleMap.keySet()) {
|
||||||
ArrayList<String> entry = (ArrayList<String>) localized.get(locale).get(key);
|
ArrayList<String> entry = (ArrayList<String>) localizedLocaleMap.get(locale);
|
||||||
if (entry != null && entry.size() > 0) {
|
if (entry != null && entry.size() > 0) {
|
||||||
String[] result = new String[entry.size()];
|
String[] result = new String[entry.size()];
|
||||||
int i = 0;
|
int i = 0;
|
||||||
@ -688,12 +618,99 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (ClassCastException e) {
|
|
||||||
Utils.debugLog(TAG, e.getMessage());
|
|
||||||
}
|
|
||||||
return new String[0];
|
return new String[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return one matching entry from the {@code localized} block in the app entry
|
||||||
|
* in the index JSON.
|
||||||
|
*/
|
||||||
|
private Map<String, Object> getLocalizedLocaleMap(Map<String, Map<String, Object>> localized,
|
||||||
|
Set<String> supportedLocales, @NonNull String key) {
|
||||||
|
String[] localesToUse = getLocalesForKey(localized, supportedLocales, key);
|
||||||
|
if (localesToUse.length > 0) {
|
||||||
|
Locale firstMatch = systemLocaleList.getFirstMatch(localesToUse);
|
||||||
|
if (firstMatch != null) {
|
||||||
|
for (String languageTag : new String[]{toLanguageTag(firstMatch), null}) {
|
||||||
|
if (languageTag == null) {
|
||||||
|
languageTag = getFallbackLanguageTag(firstMatch, localesToUse); // NOPMD
|
||||||
|
}
|
||||||
|
Map<String, Object> localeEntry = localized.get(languageTag);
|
||||||
|
if (localeEntry != null && localeEntry.containsKey(key)) {
|
||||||
|
Object value = localeEntry.get(key);
|
||||||
|
if (value != null) {
|
||||||
|
Map<String, Object> localizedLocaleMap = new HashMap<>();
|
||||||
|
localizedLocaleMap.put(languageTag, value);
|
||||||
|
return localizedLocaleMap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace with {@link Locale#toLanguageTag()} once
|
||||||
|
* {@link android.os.Build.VERSION_CODES#LOLLIPOP} is {@code minSdkVersion}
|
||||||
|
*/
|
||||||
|
private String toLanguageTag(Locale firstMatch) {
|
||||||
|
if (Build.VERSION.SDK_INT < 21) {
|
||||||
|
return firstMatch.toString().replace("_", "-");
|
||||||
|
} else {
|
||||||
|
return firstMatch.toLanguageTag();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all locales that have an entry for {@code key}.
|
||||||
|
*/
|
||||||
|
private String[] getLocalesForKey(Map<String, Map<String, Object>> localized,
|
||||||
|
Set<String> supportedLocales, @NonNull String key) {
|
||||||
|
Set<String> localesToUse = new HashSet<>();
|
||||||
|
for (String locale : supportedLocales) {
|
||||||
|
Map<String, Object> localeEntry = localized.get(locale);
|
||||||
|
if (localeEntry != null && localeEntry.get(key) != null) {
|
||||||
|
localesToUse.add(locale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return localesToUse.toArray(new String[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look for the first language-country match for languages with multiple scripts.
|
||||||
|
* Then look for a language-only match, for when there is no exact
|
||||||
|
* {@link Locale} match. Then try a locale with the same language, but
|
||||||
|
* different country. If there are still no matches, return the {@code en-US}
|
||||||
|
* entry. If all else fails, try to return the first existing English locale.
|
||||||
|
*/
|
||||||
|
private String getFallbackLanguageTag(Locale firstMatch, String[] localesToUse) {
|
||||||
|
final String firstMatchLanguageCountry = firstMatch.getLanguage() + "-" + firstMatch.getCountry();
|
||||||
|
for (String languageTag : localesToUse) {
|
||||||
|
if (languageTag.equals(firstMatchLanguageCountry)) {
|
||||||
|
return languageTag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final String firstMatchLanguage = firstMatch.getLanguage();
|
||||||
|
String englishLastResort = null;
|
||||||
|
for (String languageTag : localesToUse) {
|
||||||
|
if (languageTag.equals(firstMatchLanguage)) {
|
||||||
|
return languageTag;
|
||||||
|
} else if ("en-US".equals(languageTag)) {
|
||||||
|
englishLastResort = languageTag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (String languageTag : localesToUse) {
|
||||||
|
String languageToUse = languageTag.split("-")[0];
|
||||||
|
if (firstMatchLanguage.equals(languageToUse)) {
|
||||||
|
return languageTag;
|
||||||
|
} else if (englishLastResort == null && "en".equals(languageToUse)) {
|
||||||
|
englishLastResort = languageTag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return englishLastResort;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the app description text with all newlines replaced by {@code <br>}
|
* Returns the app description text with all newlines replaced by {@code <br>}
|
||||||
*/
|
*/
|
||||||
|
@ -6,10 +6,10 @@ import android.content.Context;
|
|||||||
import android.content.UriMatcher;
|
import android.content.UriMatcher;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import org.fdroid.fdroid.Preferences;
|
import org.fdroid.fdroid.Preferences;
|
||||||
import org.fdroid.fdroid.Utils;
|
import org.fdroid.fdroid.Utils;
|
||||||
import org.fdroid.fdroid.data.Schema.ApkAntiFeatureJoinTable;
|
import org.fdroid.fdroid.data.Schema.ApkAntiFeatureJoinTable;
|
||||||
@ -446,7 +446,7 @@ public class AppProvider extends FDroidProvider {
|
|||||||
private static final String PATH_SEARCH_REPO = "searchRepo";
|
private static final String PATH_SEARCH_REPO = "searchRepo";
|
||||||
protected static final String PATH_APPS = "apps";
|
protected static final String PATH_APPS = "apps";
|
||||||
protected static final String PATH_SPECIFIC_APP = "app";
|
protected static final String PATH_SPECIFIC_APP = "app";
|
||||||
private static final String PATH_RECENTLY_UPDATED = "recentlyUpdated";
|
private static final String PATH_LATEST_TAB = "recentlyUpdated";
|
||||||
private static final String PATH_CATEGORY = "category";
|
private static final String PATH_CATEGORY = "category";
|
||||||
private static final String PATH_REPO = "repo";
|
private static final String PATH_REPO = "repo";
|
||||||
private static final String PATH_HIGHEST_PRIORITY = "highestPriority";
|
private static final String PATH_HIGHEST_PRIORITY = "highestPriority";
|
||||||
@ -459,8 +459,8 @@ public class AppProvider extends FDroidProvider {
|
|||||||
private static final int INSTALLED = CAN_UPDATE + 1;
|
private static final int INSTALLED = CAN_UPDATE + 1;
|
||||||
private static final int SEARCH_TEXT = INSTALLED + 1;
|
private static final int SEARCH_TEXT = INSTALLED + 1;
|
||||||
private static final int SEARCH_TEXT_AND_CATEGORIES = SEARCH_TEXT + 1;
|
private static final int SEARCH_TEXT_AND_CATEGORIES = SEARCH_TEXT + 1;
|
||||||
private static final int RECENTLY_UPDATED = SEARCH_TEXT_AND_CATEGORIES + 1;
|
private static final int LATEST_TAB = SEARCH_TEXT_AND_CATEGORIES + 1;
|
||||||
private static final int CATEGORY = RECENTLY_UPDATED + 1;
|
private static final int CATEGORY = LATEST_TAB + 1;
|
||||||
private static final int CALC_SUGGESTED_APKS = CATEGORY + 1;
|
private static final int CALC_SUGGESTED_APKS = CATEGORY + 1;
|
||||||
private static final int REPO = CALC_SUGGESTED_APKS + 1;
|
private static final int REPO = CALC_SUGGESTED_APKS + 1;
|
||||||
private static final int SEARCH_REPO = REPO + 1;
|
private static final int SEARCH_REPO = REPO + 1;
|
||||||
@ -473,7 +473,7 @@ public class AppProvider extends FDroidProvider {
|
|||||||
MATCHER.addURI(getAuthority(), null, CODE_LIST);
|
MATCHER.addURI(getAuthority(), null, CODE_LIST);
|
||||||
MATCHER.addURI(getAuthority(), PATH_CALC_SUGGESTED_APKS, CALC_SUGGESTED_APKS);
|
MATCHER.addURI(getAuthority(), PATH_CALC_SUGGESTED_APKS, CALC_SUGGESTED_APKS);
|
||||||
MATCHER.addURI(getAuthority(), PATH_CALC_SUGGESTED_APKS + "/*", CALC_SUGGESTED_APKS);
|
MATCHER.addURI(getAuthority(), PATH_CALC_SUGGESTED_APKS + "/*", CALC_SUGGESTED_APKS);
|
||||||
MATCHER.addURI(getAuthority(), PATH_RECENTLY_UPDATED, RECENTLY_UPDATED);
|
MATCHER.addURI(getAuthority(), PATH_LATEST_TAB, LATEST_TAB);
|
||||||
MATCHER.addURI(getAuthority(), PATH_CATEGORY + "/*", CATEGORY);
|
MATCHER.addURI(getAuthority(), PATH_CATEGORY + "/*", CATEGORY);
|
||||||
MATCHER.addURI(getAuthority(), PATH_SEARCH + "/*/*", SEARCH_TEXT_AND_CATEGORIES);
|
MATCHER.addURI(getAuthority(), PATH_SEARCH + "/*/*", SEARCH_TEXT_AND_CATEGORIES);
|
||||||
MATCHER.addURI(getAuthority(), PATH_SEARCH + "/*", SEARCH_TEXT);
|
MATCHER.addURI(getAuthority(), PATH_SEARCH + "/*", SEARCH_TEXT);
|
||||||
@ -492,8 +492,14 @@ public class AppProvider extends FDroidProvider {
|
|||||||
return Uri.parse("content://" + getAuthority());
|
return Uri.parse("content://" + getAuthority());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Uri getRecentlyUpdatedUri() {
|
/**
|
||||||
return Uri.withAppendedPath(getContentUri(), PATH_RECENTLY_UPDATED);
|
* Get entries that are sorted by the {@link Schema.AppMetadataTable.Cols#LAST_UPDATED}
|
||||||
|
* date.
|
||||||
|
*
|
||||||
|
* @see #LATEST_TAB
|
||||||
|
*/
|
||||||
|
public static Uri getLatestTabUri() {
|
||||||
|
return Uri.withAppendedPath(getContentUri(), PATH_LATEST_TAB);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Uri calcSuggestedApksUri() {
|
private static Uri calcSuggestedApksUri() {
|
||||||
@ -845,12 +851,37 @@ public class AppProvider extends FDroidProvider {
|
|||||||
includeSwap = false;
|
includeSwap = false;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case RECENTLY_UPDATED:
|
case LATEST_TAB:
|
||||||
String table = getTableName();
|
/* Sort by localized first so users see entries in their language,
|
||||||
String isNew = table + "." + Cols.LAST_UPDATED + " <= " + table + "." + Cols.ADDED + " DESC";
|
* then sort by highlighted fields, then sort by whether the app is new,
|
||||||
String hasFeatureGraphic = table + "." + Cols.FEATURE_GRAPHIC + " IS NULL ASC ";
|
* then if it has WhatsNew/Changelog entries, then by when it was last
|
||||||
String lastUpdated = table + "." + Cols.LAST_UPDATED + " DESC";
|
* updated. Last, it sorts by the date the app was added, putting older
|
||||||
sortOrder = lastUpdated + ", " + isNew + ", " + hasFeatureGraphic;
|
* ones first, to give preference to apps that have been maintained in
|
||||||
|
* F-Droid longer.
|
||||||
|
*/
|
||||||
|
final String table = getTableName();
|
||||||
|
final String added = table + "." + Cols.ADDED;
|
||||||
|
final String lastUpdated = table + "." + Cols.LAST_UPDATED;
|
||||||
|
sortOrder = table + "." + Cols.IS_LOCALIZED + " DESC"
|
||||||
|
+ ", " + table + "." + Cols.NAME + " IS NULL ASC"
|
||||||
|
+ ", " + table + "." + Cols.ICON + " IS NULL ASC"
|
||||||
|
+ ", " + table + "." + Cols.SUMMARY + " IS NULL ASC"
|
||||||
|
+ ", " + table + "." + Cols.DESCRIPTION + " IS NULL ASC"
|
||||||
|
+ ", CASE WHEN " + table + "." + Cols.PHONE_SCREENSHOTS + " IS NULL"
|
||||||
|
+ " AND " + table + "." + Cols.SEVEN_INCH_SCREENSHOTS + " IS NULL"
|
||||||
|
+ " AND " + table + "." + Cols.TEN_INCH_SCREENSHOTS + " IS NULL"
|
||||||
|
+ " AND " + table + "." + Cols.TV_SCREENSHOTS + " IS NULL"
|
||||||
|
+ " AND " + table + "." + Cols.WEAR_SCREENSHOTS + " IS NULL"
|
||||||
|
+ " AND " + table + "." + Cols.FEATURE_GRAPHIC + " IS NULL"
|
||||||
|
+ " AND " + table + "." + Cols.PROMO_GRAPHIC + " IS NULL"
|
||||||
|
+ " AND " + table + "." + Cols.TV_BANNER + " IS NULL"
|
||||||
|
+ " THEN 1 ELSE 0 END"
|
||||||
|
+ ", CASE WHEN date(" + added + ") >= date(" + lastUpdated + ")"
|
||||||
|
+ " AND date('now','-7 days') < date(" + lastUpdated + ")"
|
||||||
|
+ " THEN 0 ELSE 1 END"
|
||||||
|
+ ", " + table + "." + Cols.WHATSNEW + " IS NULL ASC"
|
||||||
|
+ ", " + lastUpdated + " DESC"
|
||||||
|
+ ", " + added + " ASC";
|
||||||
|
|
||||||
// There seems no reason to limit the number of apps on the front page, but it helps
|
// There seems no reason to limit the number of apps on the front page, but it helps
|
||||||
// if it loads quickly, as it is the default view shown every time F-Droid is opened.
|
// if it loads quickly, as it is the default view shown every time F-Droid is opened.
|
||||||
@ -1012,16 +1043,16 @@ public class AppProvider extends FDroidProvider {
|
|||||||
/**
|
/**
|
||||||
* If the repo hasn't changed, then there are many things which we shouldn't waste time updating
|
* If the repo hasn't changed, then there are many things which we shouldn't waste time updating
|
||||||
* (compared to {@link AppProvider#updateAllAppDetails()}:
|
* (compared to {@link AppProvider#updateAllAppDetails()}:
|
||||||
|
* <ul>
|
||||||
|
* <li>The "preferred metadata", as that is calculated based on repo with highest priority, and
|
||||||
|
* only takes into account the package name, not specific versions, when figuring this out.</li>
|
||||||
*
|
*
|
||||||
* + The "preferred metadata", as that is calculated based on repo with highest priority, and
|
* <li>Compatible flags. These were calculated earlier, whether or not an app was suggested or not.</li>
|
||||||
* only takes into account the package name, not specific versions, when figuring this out.
|
|
||||||
*
|
*
|
||||||
* + Compatible flags. These were calculated earlier, whether or not an app was suggested or not.
|
* <li>Icon URLs. While technically these do change when the suggested version changes, it is not
|
||||||
*
|
|
||||||
* + Icon URLs. While technically these do change when the suggested version changes, it is not
|
|
||||||
* important enough to spend a significant amount of time to calculate. In the future maybe,
|
* important enough to spend a significant amount of time to calculate. In the future maybe,
|
||||||
* but that effort should instead go into implementing an intent service.
|
* but that effort should instead go into implementing an intent service.</li>
|
||||||
*
|
* </ul>
|
||||||
* In the future, this problem of taking a long time should be fixed by implementing an
|
* In the future, this problem of taking a long time should be fixed by implementing an
|
||||||
* {@link android.app.IntentService} as described in https://gitlab.com/fdroid/fdroidclient/issues/520.
|
* {@link android.app.IntentService} as described in https://gitlab.com/fdroid/fdroidclient/issues/520.
|
||||||
*/
|
*/
|
||||||
@ -1139,7 +1170,7 @@ public class AppProvider extends FDroidProvider {
|
|||||||
/**
|
/**
|
||||||
* We set each app's suggested version to the latest available that is
|
* We set each app's suggested version to the latest available that is
|
||||||
* compatible, or the latest available if none are compatible.
|
* compatible, or the latest available if none are compatible.
|
||||||
*
|
* <p>
|
||||||
* If the suggested version is null, it means that we could not figure it
|
* If the suggested version is null, it means that we could not figure it
|
||||||
* out from the upstream vercode. In such a case, fall back to the simpler
|
* out from the upstream vercode. In such a case, fall back to the simpler
|
||||||
* algorithm as if upstreamVercode was 0.
|
* algorithm as if upstreamVercode was 0.
|
||||||
|
@ -209,6 +209,13 @@ public interface Schema {
|
|||||||
String TV_SCREENSHOTS = "tvScreenshots";
|
String TV_SCREENSHOTS = "tvScreenshots";
|
||||||
String WEAR_SCREENSHOTS = "wearScreenshots";
|
String WEAR_SCREENSHOTS = "wearScreenshots";
|
||||||
String IS_APK = "isApk";
|
String IS_APK = "isApk";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Has this {@code App} been localized into one of the user's current locales.
|
||||||
|
*
|
||||||
|
* @see App#setIsLocalized(java.util.Set)
|
||||||
|
* @see org.fdroid.fdroid.views.main.WhatsNewViewBinder#onCreateLoader(int, android.os.Bundle)
|
||||||
|
*/
|
||||||
String IS_LOCALIZED = "isLocalized";
|
String IS_LOCALIZED = "isLocalized";
|
||||||
|
|
||||||
interface AutoInstallApk {
|
interface AutoInstallApk {
|
||||||
|
@ -3,20 +3,20 @@ package org.fdroid.fdroid.views.main;
|
|||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
|
||||||
import androidx.loader.app.LoaderManager;
|
|
||||||
import androidx.loader.content.CursorLoader;
|
|
||||||
import androidx.loader.content.Loader;
|
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.loader.app.LoaderManager;
|
||||||
|
import androidx.loader.content.CursorLoader;
|
||||||
|
import androidx.loader.content.Loader;
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||||
|
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||||
import org.fdroid.fdroid.Preferences;
|
import org.fdroid.fdroid.Preferences;
|
||||||
import org.fdroid.fdroid.R;
|
import org.fdroid.fdroid.R;
|
||||||
import org.fdroid.fdroid.UpdateService;
|
import org.fdroid.fdroid.UpdateService;
|
||||||
@ -24,12 +24,11 @@ import org.fdroid.fdroid.Utils;
|
|||||||
import org.fdroid.fdroid.data.AppProvider;
|
import org.fdroid.fdroid.data.AppProvider;
|
||||||
import org.fdroid.fdroid.data.RepoProvider;
|
import org.fdroid.fdroid.data.RepoProvider;
|
||||||
import org.fdroid.fdroid.data.Schema.AppMetadataTable;
|
import org.fdroid.fdroid.data.Schema.AppMetadataTable;
|
||||||
import org.fdroid.fdroid.views.apps.AppListActivity;
|
|
||||||
import org.fdroid.fdroid.panic.HidingManager;
|
import org.fdroid.fdroid.panic.HidingManager;
|
||||||
|
import org.fdroid.fdroid.views.apps.AppListActivity;
|
||||||
import org.fdroid.fdroid.views.whatsnew.WhatsNewAdapter;
|
import org.fdroid.fdroid.views.whatsnew.WhatsNewAdapter;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads a list of newly added or recently updated apps and displays them to the user.
|
* Loads a list of newly added or recently updated apps and displays them to the user.
|
||||||
@ -95,38 +94,20 @@ class WhatsNewViewBinder implements LoaderManager.LoaderCallbacks<Cursor> {
|
|||||||
activity.getSupportLoaderManager().initLoader(LOADER_ID, null, this);
|
activity.getSupportLoaderManager().initLoader(LOADER_ID, null, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see AppProvider#getLatestTabUri()
|
||||||
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||||
if (id != LOADER_ID) {
|
if (id != LOADER_ID) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// select that have all required items:
|
|
||||||
String selection = "(" + AppMetadataTable.NAME + "." + AppMetadataTable.Cols.NAME + " != ''"
|
|
||||||
+ " AND " + AppMetadataTable.NAME + "." + AppMetadataTable.Cols.SUMMARY + " != ''"
|
|
||||||
+ " AND " + AppMetadataTable.NAME + "." + AppMetadataTable.Cols.DESCRIPTION + " != ''"
|
|
||||||
+ " AND " + AppMetadataTable.NAME + "." + AppMetadataTable.Cols.LICENSE + " != ''"
|
|
||||||
+ " AND " + AppMetadataTable.NAME + "." + AppMetadataTable.Cols.WHATSNEW + " != ''";
|
|
||||||
if (!"en".equals(Locale.getDefault().getLanguage())) {
|
|
||||||
// only require localization if using a non-English locale
|
|
||||||
selection += " AND " + AppMetadataTable.NAME + "." + AppMetadataTable.Cols.IS_LOCALIZED + " = 1";
|
|
||||||
}
|
|
||||||
// and at least one optional item:
|
|
||||||
selection += ") AND ("
|
|
||||||
+ AppMetadataTable.NAME + "." + AppMetadataTable.Cols.SEVEN_INCH_SCREENSHOTS + " IS NOT NULL "
|
|
||||||
+ " OR " + AppMetadataTable.NAME + "." + AppMetadataTable.Cols.PHONE_SCREENSHOTS + " IS NOT NULL "
|
|
||||||
+ " OR " + AppMetadataTable.NAME + "." + AppMetadataTable.Cols.TEN_INCH_SCREENSHOTS + " IS NOT NULL "
|
|
||||||
+ " OR " + AppMetadataTable.NAME + "." + AppMetadataTable.Cols.TV_SCREENSHOTS + " IS NOT NULL "
|
|
||||||
+ " OR " + AppMetadataTable.NAME + "." + AppMetadataTable.Cols.WEAR_SCREENSHOTS + " IS NOT NULL "
|
|
||||||
+ " OR " + AppMetadataTable.NAME + "." + AppMetadataTable.Cols.FEATURE_GRAPHIC + " IS NOT NULL "
|
|
||||||
+ ")";
|
|
||||||
|
|
||||||
return new CursorLoader(
|
return new CursorLoader(
|
||||||
activity,
|
activity,
|
||||||
AppProvider.getRecentlyUpdatedUri(),
|
AppProvider.getLatestTabUri(),
|
||||||
AppMetadataTable.Cols.ALL,
|
AppMetadataTable.Cols.ALL,
|
||||||
selection,
|
null,
|
||||||
null,
|
null,
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
@ -18,10 +18,8 @@ import org.robolectric.RobolectricTestRunner;
|
|||||||
import org.robolectric.annotation.Config;
|
import org.robolectric.annotation.Config;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.TimeZone;
|
import java.util.TimeZone;
|
||||||
|
|
||||||
import static org.fdroid.fdroid.Assert.assertContainsOnly;
|
import static org.fdroid.fdroid.Assert.assertContainsOnly;
|
||||||
@ -280,58 +278,6 @@ public class AppProviderTest extends FDroidProviderTest {
|
|||||||
assertEquals("F-Droid", otherApp.name);
|
assertEquals("F-Droid", otherApp.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testAppSetLocalized() {
|
|
||||||
final String enSummary = "utility for getting information about the APKs that are installed on your device";
|
|
||||||
HashMap<String, Object> en = new HashMap<>();
|
|
||||||
en.put("summary", enSummary);
|
|
||||||
|
|
||||||
final String esSummary = "utilidad para obtener información sobre los APKs instalados en su dispositivo";
|
|
||||||
HashMap<String, Object> es = new HashMap<>();
|
|
||||||
es.put("summary", esSummary);
|
|
||||||
|
|
||||||
final String frSummary = "utilitaire pour obtenir des informations sur les APKs qui sont installés sur vot";
|
|
||||||
HashMap<String, Object> fr = new HashMap<>();
|
|
||||||
fr.put("summary", frSummary);
|
|
||||||
|
|
||||||
final String nlSummary = "hulpprogramma voor het verkrijgen van informatie over de APK die zijn geïnstalle";
|
|
||||||
HashMap<String, Object> nl = new HashMap<>();
|
|
||||||
nl.put("summary", nlSummary);
|
|
||||||
|
|
||||||
App app = new App();
|
|
||||||
Map<String, Map<String, Object>> localized = new HashMap<>();
|
|
||||||
localized.put("es", es);
|
|
||||||
localized.put("fr", fr);
|
|
||||||
|
|
||||||
Locale.setDefault(new Locale("nl", "NL"));
|
|
||||||
app.setLocalized(localized);
|
|
||||||
assertFalse(app.isLocalized);
|
|
||||||
|
|
||||||
localized.put("nl", nl);
|
|
||||||
app.setLocalized(localized);
|
|
||||||
assertTrue(app.isLocalized);
|
|
||||||
assertEquals(nlSummary, app.summary);
|
|
||||||
|
|
||||||
app = new App();
|
|
||||||
localized.clear();
|
|
||||||
localized.put("nl", nl);
|
|
||||||
app.setLocalized(localized);
|
|
||||||
assertTrue(app.isLocalized);
|
|
||||||
|
|
||||||
app = new App();
|
|
||||||
localized.clear();
|
|
||||||
localized.put("en-US", en);
|
|
||||||
app.setLocalized(localized);
|
|
||||||
assertFalse(app.isLocalized);
|
|
||||||
|
|
||||||
Locale.setDefault(new Locale("en", "US"));
|
|
||||||
app = new App();
|
|
||||||
localized.clear();
|
|
||||||
localized.put("en-US", en);
|
|
||||||
app.setLocalized(localized);
|
|
||||||
assertTrue(app.isLocalized);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testInsertTrimsNamesAndSummary() {
|
public void testInsertTrimsNamesAndSummary() {
|
||||||
// Insert a new record with unwanted newlines...
|
// Insert a new record with unwanted newlines...
|
||||||
|
@ -1,23 +1,27 @@
|
|||||||
package org.fdroid.fdroid.data;
|
package org.fdroid.fdroid.data;
|
||||||
|
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.os.Build;
|
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.fdroid.fdroid.TestUtils;
|
||||||
|
import org.junit.Assume;
|
||||||
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.robolectric.RobolectricTestRunner;
|
import org.robolectric.RobolectricTestRunner;
|
||||||
|
import org.robolectric.shadows.ShadowLog;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertTrue;
|
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)
|
@RunWith(RobolectricTestRunner.class)
|
||||||
@SuppressWarnings("LocalVariableName")
|
@SuppressWarnings("LocalVariableName")
|
||||||
@ -25,83 +29,25 @@ public class LocaleSelectionTest {
|
|||||||
|
|
||||||
private static final String KEY = "summary";
|
private static final String KEY = "summary";
|
||||||
|
|
||||||
@Test
|
private static final String EN_US_NAME = "Checkey: info on local apps";
|
||||||
public void correctLocaleSelectionBeforeSDK24() throws Exception {
|
private static final String EN_US_FEATURE_GRAPHIC = "en-US/featureGraphic.png";
|
||||||
TestUtils.setFinalStatic(Build.VERSION.class.getDeclaredField("SDK_INT"), 19);
|
private static final String EN_US_PHONE_SCREENSHOT = "en-US/phoneScreenshots/First.png";
|
||||||
assertTrue(Build.VERSION.SDK_INT < 24);
|
private static final String EN_US_SEVEN_INCH_SCREENSHOT = "en-US/sevenInchScreenshots/checkey-tablet.png";
|
||||||
App app;
|
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<>();
|
@Before
|
||||||
HashMap<String, Object> en_US = new HashMap<>();
|
public final void setUp() {
|
||||||
en_US.put(KEY, "summary-en_US");
|
ShadowLog.stream = System.out;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void correctLocaleSelectionFromSDK24() throws Exception {
|
public void localeSelection() throws Exception {
|
||||||
|
|
||||||
TestUtils.setFinalStatic(Build.VERSION.class.getDeclaredField("SDK_INT"), 29);
|
App app = new App();
|
||||||
assertTrue(Build.VERSION.SDK_INT >= 24);
|
App.systemLocaleList = LocaleListCompat.forLanguageTags("en-US,de-DE");
|
||||||
|
|
||||||
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");
|
|
||||||
|
|
||||||
//no metadata present
|
//no metadata present
|
||||||
Map<String, Map<String, Object>> localized = new HashMap<>();
|
Map<String, Map<String, Object>> localized = new HashMap<>();
|
||||||
@ -112,10 +58,18 @@ public class LocaleSelectionTest {
|
|||||||
en_US.put(KEY, "summary-en_US");
|
en_US.put(KEY, "summary-en_US");
|
||||||
HashMap<String, Object> en_GB = new HashMap<>();
|
HashMap<String, Object> en_GB = new HashMap<>();
|
||||||
en_GB.put(KEY, "summary-en_GB");
|
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<>();
|
HashMap<String, Object> de_AT = new HashMap<>();
|
||||||
de_AT.put(KEY, "summary-de_AT");
|
de_AT.put(KEY, "summary-de_AT");
|
||||||
HashMap<String, Object> de_DE = new HashMap<>();
|
HashMap<String, Object> de_DE = new HashMap<>();
|
||||||
de_DE.put(KEY, "summary-de_DE");
|
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";
|
app.summary = "reset";
|
||||||
localized.put("de-AT", de_AT);
|
localized.put("de-AT", de_AT);
|
||||||
@ -125,8 +79,7 @@ public class LocaleSelectionTest {
|
|||||||
// just select the matching en-US locale, nothing special here
|
// just select the matching en-US locale, nothing special here
|
||||||
assertEquals(en_US.get(KEY), app.summary);
|
assertEquals(en_US.get(KEY), app.summary);
|
||||||
|
|
||||||
Locale.setDefault(new Locale("en", "SE"));
|
App.systemLocaleList = LocaleListCompat.forLanguageTags("en-SE,de-DE");
|
||||||
when(localeList.toLanguageTags()).thenReturn("en-SE,de-DE");
|
|
||||||
app.setLocalized(localized);
|
app.setLocalized(localized);
|
||||||
// Fall back to another en locale before de
|
// Fall back to another en locale before de
|
||||||
assertEquals(en_US.get(KEY), app.summary);
|
assertEquals(en_US.get(KEY), app.summary);
|
||||||
@ -138,8 +91,7 @@ public class LocaleSelectionTest {
|
|||||||
localized.put("en-GB", en_GB);
|
localized.put("en-GB", en_GB);
|
||||||
localized.put("en-US", en_US);
|
localized.put("en-US", en_US);
|
||||||
|
|
||||||
Locale.setDefault(new Locale("de", "AT"));
|
App.systemLocaleList = LocaleListCompat.forLanguageTags("de-AT,de-DE");
|
||||||
when(localeList.toLanguageTags()).thenReturn("de-AT,de-DE");
|
|
||||||
app.setLocalized(localized);
|
app.setLocalized(localized);
|
||||||
// full match against a non-default locale
|
// full match against a non-default locale
|
||||||
assertEquals(de_AT.get(KEY), app.summary);
|
assertEquals(de_AT.get(KEY), app.summary);
|
||||||
@ -147,14 +99,13 @@ public class LocaleSelectionTest {
|
|||||||
app.summary = "reset";
|
app.summary = "reset";
|
||||||
localized.clear();
|
localized.clear();
|
||||||
localized.put("de-AT", de_AT);
|
localized.put("de-AT", de_AT);
|
||||||
localized.put("de", de_DE);
|
localized.put("de", de);
|
||||||
localized.put("en-GB", en_GB);
|
localized.put("en-GB", en_GB);
|
||||||
localized.put("en-US", en_US);
|
localized.put("en-US", en_US);
|
||||||
|
|
||||||
Locale.setDefault(new Locale("de", "CH"));
|
App.systemLocaleList = LocaleListCompat.forLanguageTags("de-CH,en-US");
|
||||||
when(localeList.toLanguageTags()).thenReturn("de-CH,en-US");
|
|
||||||
app.setLocalized(localized);
|
app.setLocalized(localized);
|
||||||
assertEquals(de_DE.get(KEY), app.summary);
|
assertEquals(de.get(KEY), app.summary);
|
||||||
|
|
||||||
app.summary = "reset";
|
app.summary = "reset";
|
||||||
localized.clear();
|
localized.clear();
|
||||||
@ -162,13 +113,12 @@ public class LocaleSelectionTest {
|
|||||||
localized.put("en-US", en_US);
|
localized.put("en-US", en_US);
|
||||||
|
|
||||||
Locale.setDefault(new Locale("en", "AU"));
|
Locale.setDefault(new Locale("en", "AU"));
|
||||||
when(localeList.toLanguageTags()).thenReturn("en-AU");
|
App.systemLocaleList = LocaleListCompat.forLanguageTags("en-AU");
|
||||||
app.setLocalized(localized);
|
app.setLocalized(localized);
|
||||||
assertEquals(en_US.get(KEY), app.summary);
|
assertEquals(en_US.get(KEY), app.summary);
|
||||||
|
|
||||||
app.summary = "reset";
|
app.summary = "reset";
|
||||||
Locale.setDefault(new Locale("zh", "TW", "#Hant"));
|
App.systemLocaleList = LocaleListCompat.forLanguageTags("zh-Hant-TW,zh-Hans-CN");
|
||||||
when(localeList.toLanguageTags()).thenReturn("zh-Hant-TW,zh-Hans-CN");
|
|
||||||
localized.clear();
|
localized.clear();
|
||||||
localized.put("en", en_GB);
|
localized.put("en", en_GB);
|
||||||
localized.put("en-US", en_US);
|
localized.put("en-US", en_US);
|
||||||
@ -197,5 +147,159 @@ public class LocaleSelectionTest {
|
|||||||
localized.put("zh-CN", zh_CN);
|
localized.put("zh-CN", zh_CN);
|
||||||
app.setLocalized(localized);
|
app.setLocalized(localized);
|
||||||
assertEquals(zh_CN.get(KEY), app.summary);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testIsLocalized() {
|
||||||
|
final String enSummary = "utility for getting information about the APKs that are installed on your device";
|
||||||
|
HashMap<String, Object> en = new HashMap<>();
|
||||||
|
en.put("summary", enSummary);
|
||||||
|
|
||||||
|
final String esSummary = "utilidad para obtener información sobre los APKs instalados en su dispositivo";
|
||||||
|
HashMap<String, Object> es = new HashMap<>();
|
||||||
|
es.put("summary", esSummary);
|
||||||
|
|
||||||
|
final String frSummary = "utilitaire pour obtenir des informations sur les APKs qui sont installés sur vot";
|
||||||
|
HashMap<String, Object> fr = new HashMap<>();
|
||||||
|
fr.put("summary", frSummary);
|
||||||
|
|
||||||
|
final String nlSummary = "hulpprogramma voor het verkrijgen van informatie over de APK die zijn geïnstalle";
|
||||||
|
HashMap<String, Object> nl = new HashMap<>();
|
||||||
|
nl.put("summary", nlSummary);
|
||||||
|
|
||||||
|
App app = new App();
|
||||||
|
Map<String, Map<String, Object>> localized = new HashMap<>();
|
||||||
|
localized.put("es", es);
|
||||||
|
localized.put("fr", fr);
|
||||||
|
|
||||||
|
App.systemLocaleList = LocaleListCompat.forLanguageTags("nl-NL");
|
||||||
|
app.setLocalized(localized);
|
||||||
|
assertFalse(app.isLocalized);
|
||||||
|
|
||||||
|
localized.put("nl", nl);
|
||||||
|
app.setLocalized(localized);
|
||||||
|
assertTrue(app.isLocalized);
|
||||||
|
assertEquals(nlSummary, app.summary);
|
||||||
|
|
||||||
|
app = new App();
|
||||||
|
localized.clear();
|
||||||
|
localized.put("nl", nl);
|
||||||
|
app.setLocalized(localized);
|
||||||
|
assertTrue(app.isLocalized);
|
||||||
|
|
||||||
|
app = new App();
|
||||||
|
localized.clear();
|
||||||
|
localized.put("en-US", en);
|
||||||
|
app.setLocalized(localized);
|
||||||
|
assertFalse(app.isLocalized);
|
||||||
|
|
||||||
|
App.systemLocaleList = LocaleListCompat.forLanguageTags("en-US");
|
||||||
|
app = new App();
|
||||||
|
localized.clear();
|
||||||
|
localized.put("en-US", en);
|
||||||
|
app.setLocalized(localized);
|
||||||
|
assertTrue(app.isLocalized);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -370,6 +370,7 @@ public class IndexV1UpdaterTest extends FDroidProviderTest {
|
|||||||
"isLocalized",
|
"isLocalized",
|
||||||
"preferredSigner",
|
"preferredSigner",
|
||||||
"prefs",
|
"prefs",
|
||||||
|
"systemLocaleList",
|
||||||
"TAG",
|
"TAG",
|
||||||
};
|
};
|
||||||
runJsonIgnoreTest(new App(), allowedInApp, ignoredInApp);
|
runJsonIgnoreTest(new App(), allowedInApp, ignoredInApp);
|
||||||
|
285
app/src/test/resources/localized.json
Normal file
285
app/src/test/resources/localized.json
Normal file
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user