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.
|
||||
|
||||
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
|
||||
were making code changes. This way the changes can be reviewed before the
|
||||
source strings on Weblate are changed.
|
||||
suggestions on how to improve them, open an issue or merge request like you
|
||||
would if you were making code changes. This way the changes can be reviewed
|
||||
before the source strings on Weblate are changed.
|
||||
|
||||
|
||||
## Code Style
|
||||
|
||||
We follow the [Android Java style](https://source.android.com/source/code-style.html).
|
||||
Some key points:
|
||||
We follow the default Android Studio code formatter (e.g. `Ctrl-Alt-L`). This
|
||||
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
|
||||
* UTF-8 source files
|
||||
@ -45,76 +47,35 @@ Some key points:
|
||||
* Braces are always used after if, for and while
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
connected via `adb`, or an emulator running. Then, execute the following from the
|
||||
command line:
|
||||
|
||||
./gradlew check
|
||||
|
||||
Note that the CI already runs the tests on an emulator, so you don't
|
||||
necessarily have to do this yourself if you open a merge request as the tests
|
||||
will get run.
|
||||
Many important tests require a device or emulator, but do not work in GitLab CI.
|
||||
That mean they need to be run locally, and that is usually easiest in Android
|
||||
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"
|
||||
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_.
|
||||
./gradlew testFullDebugUnitTest --tests *LocaleSelectionTest*
|
||||
|
||||
## 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
|
||||
stable has an important bug that needs immediate fixing - will follow the
|
||||
`X.Y.Z` pattern.
|
||||
./gradlew connectedFullDebugAndroidTest \
|
||||
-Pandroid.testInstrumentationRunnerArguments.class=org.fdroid.fdroid.MainActivityExpressoTest
|
||||
|
||||
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`.
|
||||
So for example, 1.3.1 would be `1003150` and 0.95-alpha13 would be `95013`
|
||||
(leading zeros are omitted).
|
||||
## Making releases
|
||||
|
||||
Note that we use a trailing `50` for actual stable releases, so alphas are
|
||||
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.
|
||||
See https://gitlab.com/fdroid/wiki/-/wikis/Internal/Release-Process#fdroidclient
|
||||
|
@ -67,6 +67,7 @@ import org.apache.commons.net.util.SubnetUtils;
|
||||
import org.fdroid.fdroid.Preferences.ChangeListener;
|
||||
import org.fdroid.fdroid.Preferences.Theme;
|
||||
import org.fdroid.fdroid.compat.PRNGFixes;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
import org.fdroid.fdroid.data.InstalledAppProviderService;
|
||||
import org.fdroid.fdroid.data.Repo;
|
||||
@ -330,6 +331,7 @@ public class FDroidApp extends Application {
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
Languages.setLanguage(this);
|
||||
App.systemLocaleList = null;
|
||||
|
||||
// update the descriptions based on the new language preferences
|
||||
SharedPreferences atStartTime = getAtStartTimeSharedPreferences();
|
||||
|
@ -19,6 +19,8 @@ import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import androidx.annotation.NonNull;
|
||||
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.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
@ -41,8 +43,8 @@ import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
@ -75,6 +77,14 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
|
||||
@JsonIgnore
|
||||
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
|
||||
/**
|
||||
* 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;
|
||||
@JsonIgnore
|
||||
public boolean isApk;
|
||||
|
||||
/**
|
||||
* Has this {@code App} been localized into one of the user's current locales.
|
||||
*/
|
||||
@JsonIgnore
|
||||
boolean isLocalized = false;
|
||||
boolean isLocalized;
|
||||
|
||||
/**
|
||||
* This is primarily for the purpose of saving app metadata when parsing an index.xml file.
|
||||
@ -484,214 +498,217 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
|
||||
* {@code localized} block is included in the index. Also, null strings in
|
||||
* the {@code localized} block should not overwrite Name/Summary/Description
|
||||
* strings with empty/null if they were set directly by Jackson.
|
||||
* <p>
|
||||
* 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>
|
||||
* <ol>
|
||||
* <li>the country variant {@code de-AT} from the user locale list
|
||||
* <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>the first available {@code en} locale
|
||||
* </ol></li>
|
||||
* <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}.
|
||||
* </ol>
|
||||
* <p>
|
||||
* On {@code < android-24}, the user can only set a single
|
||||
* locale with a country as an option, so here it makes sense to try to fallback
|
||||
* on other country-specific locales, rather than English.
|
||||
* The system-wide language preference list was added in {@code android-24}.
|
||||
*
|
||||
* @see <a href="https://developer.android.com/guide/topics/resources/multilingual-support">Android language and locale resolution overview</a>
|
||||
*/
|
||||
@JsonProperty("localized")
|
||||
void setLocalized(Map<String, Map<String, Object>> localized) { // NOPMD
|
||||
Locale defaultLocale = Locale.getDefault();
|
||||
String languageTag = defaultLocale.getLanguage();
|
||||
String countryTag = defaultLocale.getCountry();
|
||||
String localeTag;
|
||||
if (TextUtils.isEmpty(countryTag)) {
|
||||
localeTag = languageTag;
|
||||
} else {
|
||||
localeTag = languageTag + "-" + countryTag;
|
||||
if (systemLocaleList == null) {
|
||||
systemLocaleList = ConfigurationCompat.getLocales(Resources.getSystem().getConfiguration());
|
||||
}
|
||||
|
||||
Set<String> availableLocales = localized.keySet();
|
||||
Set<String> localesToUse = new LinkedHashSet<>();
|
||||
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");
|
||||
Set<String> supportedLocales = localized.keySet();
|
||||
setIsLocalized(supportedLocales);
|
||||
String value = getLocalizedEntry(localized, supportedLocales, "whatsNew");
|
||||
if (!TextUtils.isEmpty(value)) {
|
||||
whatsNew = value;
|
||||
}
|
||||
value = getLocalizedEntry(localized, localesToUse, "video");
|
||||
|
||||
value = getLocalizedEntry(localized, supportedLocales, "video");
|
||||
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)) {
|
||||
name = value;
|
||||
name = value.trim();
|
||||
}
|
||||
value = getLocalizedEntry(localized, localesToUse, "summary");
|
||||
value = getLocalizedEntry(localized, supportedLocales, "summary");
|
||||
if (!TextUtils.isEmpty(value)) {
|
||||
summary = value;
|
||||
summary = value.trim();
|
||||
}
|
||||
value = getLocalizedEntry(localized, localesToUse, "description");
|
||||
value = getLocalizedEntry(localized, supportedLocales, "description");
|
||||
if (!TextUtils.isEmpty(value)) {
|
||||
description = formatDescription(value);
|
||||
}
|
||||
value = getLocalizedGraphicsEntry(localized, localesToUse, "icon");
|
||||
value = getLocalizedGraphicsEntry(localized, supportedLocales, "icon");
|
||||
if (!TextUtils.isEmpty(value)) {
|
||||
iconUrl = value;
|
||||
}
|
||||
|
||||
featureGraphic = getLocalizedGraphicsEntry(localized, localesToUse, "featureGraphic");
|
||||
promoGraphic = getLocalizedGraphicsEntry(localized, localesToUse, "promoGraphic");
|
||||
tvBanner = getLocalizedGraphicsEntry(localized, localesToUse, "tvBanner");
|
||||
featureGraphic = getLocalizedGraphicsEntry(localized, supportedLocales, "featureGraphic");
|
||||
promoGraphic = getLocalizedGraphicsEntry(localized, supportedLocales, "promoGraphic");
|
||||
tvBanner = getLocalizedGraphicsEntry(localized, supportedLocales, "tvBanner");
|
||||
|
||||
wearScreenshots = getLocalizedListEntry(localized, localesToUse, "wearScreenshots");
|
||||
phoneScreenshots = getLocalizedListEntry(localized, localesToUse, "phoneScreenshots");
|
||||
sevenInchScreenshots = getLocalizedListEntry(localized, localesToUse, "sevenInchScreenshots");
|
||||
tenInchScreenshots = getLocalizedListEntry(localized, localesToUse, "tenInchScreenshots");
|
||||
tvScreenshots = getLocalizedListEntry(localized, localesToUse, "tvScreenshots");
|
||||
wearScreenshots = getLocalizedListEntry(localized, supportedLocales, "wearScreenshots");
|
||||
phoneScreenshots = getLocalizedListEntry(localized, supportedLocales, "phoneScreenshots");
|
||||
sevenInchScreenshots = getLocalizedListEntry(localized, supportedLocales, "sevenInchScreenshots");
|
||||
tenInchScreenshots = getLocalizedListEntry(localized, supportedLocales, "tenInchScreenshots");
|
||||
tvScreenshots = getLocalizedListEntry(localized, supportedLocales, "tvScreenshots");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* "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()}
|
||||
* as the first entry, then sorting the rest based on length (e.g. {@code de-AT}
|
||||
* before {@code de}).
|
||||
* Sets the boolean flag {@link #isLocalized} if this app entry has an localized
|
||||
* entry in one of the user's current locales.
|
||||
*
|
||||
* @see LocaleList
|
||||
* @see Locale#getDefault()
|
||||
* @see java.util.Locale.LanguageRange
|
||||
* @see org.fdroid.fdroid.views.main.WhatsNewViewBinder#onCreateLoader(int, android.os.Bundle)
|
||||
*/
|
||||
private String getLocalizedEntry(Map<String, Map<String, Object>> localized,
|
||||
Set<String> locales, String key) {
|
||||
try {
|
||||
for (String locale : locales) {
|
||||
if (localized.containsKey(locale)) {
|
||||
String value = (String) localized.get(locale).get(key);
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
} catch (ClassCastException e) {
|
||||
Utils.debugLog(TAG, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the right localized version of this entry, based on an imitation of
|
||||
* the logic that Android uses.
|
||||
*
|
||||
* @see LocaleList
|
||||
*/
|
||||
private String getLocalizedEntry(Map<String, Map<String, Object>> localized,
|
||||
Set<String> supportedLocales, @NonNull String key) {
|
||||
Map<String, Object> localizedLocaleMap = getLocalizedLocaleMap(localized, supportedLocales, key);
|
||||
if (localizedLocaleMap != null && !localizedLocaleMap.isEmpty()) {
|
||||
for (Object entry : localizedLocaleMap.values()) {
|
||||
return (String) entry; // NOPMD
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getLocalizedGraphicsEntry(Map<String, Map<String, Object>> localized,
|
||||
Set<String> locales, String key) {
|
||||
try {
|
||||
for (String locale : locales) {
|
||||
Map<String, Object> entry = localized.get(locale);
|
||||
if (entry != null) {
|
||||
Object value = entry.get(key);
|
||||
if (value != null && value.toString().length() > 0) {
|
||||
return locale + "/" + value;
|
||||
}
|
||||
}
|
||||
Set<String> supportedLocales, @NonNull String key) {
|
||||
Map<String, Object> localizedLocaleMap = getLocalizedLocaleMap(localized, supportedLocales, key);
|
||||
if (localizedLocaleMap != null && !localizedLocaleMap.isEmpty()) {
|
||||
for (String locale : localizedLocaleMap.keySet()) {
|
||||
return locale + "/" + localizedLocaleMap.get(locale); // NOPMD
|
||||
}
|
||||
} catch (ClassCastException e) {
|
||||
Utils.debugLog(TAG, e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String[] getLocalizedListEntry(Map<String, Map<String, Object>> localized,
|
||||
Set<String> locales, String key) {
|
||||
try {
|
||||
for (String locale : locales) {
|
||||
if (localized.containsKey(locale)) {
|
||||
ArrayList<String> entry = (ArrayList<String>) localized.get(locale).get(key);
|
||||
if (entry != null && entry.size() > 0) {
|
||||
String[] result = new String[entry.size()];
|
||||
int i = 0;
|
||||
for (String e : entry) {
|
||||
result[i] = locale + "/" + key + "/" + e;
|
||||
i++;
|
||||
Set<String> supportedLocales, @NonNull String key) {
|
||||
Map<String, Object> localizedLocaleMap = getLocalizedLocaleMap(localized, supportedLocales, key);
|
||||
if (localizedLocaleMap != null && !localizedLocaleMap.isEmpty()) {
|
||||
for (String locale : localizedLocaleMap.keySet()) {
|
||||
ArrayList<String> entry = (ArrayList<String>) localizedLocaleMap.get(locale);
|
||||
if (entry != null && entry.size() > 0) {
|
||||
String[] result = new String[entry.size()];
|
||||
int i = 0;
|
||||
for (String e : entry) {
|
||||
result[i] = locale + "/" + key + "/" + e;
|
||||
i++;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
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 result;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (ClassCastException e) {
|
||||
Utils.debugLog(TAG, e.getMessage());
|
||||
}
|
||||
return new String[0];
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -6,10 +6,10 @@ import android.content.Context;
|
||||
import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import org.fdroid.fdroid.Preferences;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
import org.fdroid.fdroid.data.Schema.ApkAntiFeatureJoinTable;
|
||||
@ -262,17 +262,17 @@ public class AppProvider extends FDroidProvider {
|
||||
|
||||
@Override
|
||||
protected String getRequiredTables() {
|
||||
final String pkg = PackageTable.NAME;
|
||||
final String app = getTableName();
|
||||
final String pkg = PackageTable.NAME;
|
||||
final String app = getTableName();
|
||||
final String repo = RepoTable.NAME;
|
||||
final String cat = CategoryTable.NAME;
|
||||
final String cat = CategoryTable.NAME;
|
||||
final String catJoin = getCatJoinTableName();
|
||||
|
||||
return pkg +
|
||||
" JOIN " + app + " ON (" + app + "." + Cols.PACKAGE_ID + " = " + pkg + "." + PackageTable.Cols.ROW_ID + ") " +
|
||||
" JOIN " + repo + " ON (" + app + "." + Cols.REPO_ID + " = " + repo + "." + RepoTable.Cols._ID + ") " +
|
||||
" LEFT JOIN " + catJoin + " ON (" + app + "." + Cols.ROW_ID + " = " + catJoin + "." + CatJoinTable.Cols.APP_METADATA_ID + ") " +
|
||||
" LEFT JOIN " + cat + " ON (" + cat + "." + CategoryTable.Cols.ROW_ID + " = " + catJoin + "." + CatJoinTable.Cols.CATEGORY_ID + ") ";
|
||||
" JOIN " + app + " ON (" + app + "." + Cols.PACKAGE_ID + " = " + pkg + "." + PackageTable.Cols.ROW_ID + ") " +
|
||||
" JOIN " + repo + " ON (" + app + "." + Cols.REPO_ID + " = " + repo + "." + RepoTable.Cols._ID + ") " +
|
||||
" LEFT JOIN " + catJoin + " ON (" + app + "." + Cols.ROW_ID + " = " + catJoin + "." + CatJoinTable.Cols.APP_METADATA_ID + ") " +
|
||||
" LEFT JOIN " + cat + " ON (" + cat + "." + CategoryTable.Cols.ROW_ID + " = " + catJoin + "." + CatJoinTable.Cols.CATEGORY_ID + ") ";
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -446,7 +446,7 @@ public class AppProvider extends FDroidProvider {
|
||||
private static final String PATH_SEARCH_REPO = "searchRepo";
|
||||
protected static final String PATH_APPS = "apps";
|
||||
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_REPO = "repo";
|
||||
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 SEARCH_TEXT = INSTALLED + 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 CATEGORY = RECENTLY_UPDATED + 1;
|
||||
private static final int LATEST_TAB = SEARCH_TEXT_AND_CATEGORIES + 1;
|
||||
private static final int CATEGORY = LATEST_TAB + 1;
|
||||
private static final int CALC_SUGGESTED_APKS = CATEGORY + 1;
|
||||
private static final int REPO = CALC_SUGGESTED_APKS + 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(), 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_SEARCH + "/*/*", SEARCH_TEXT_AND_CATEGORIES);
|
||||
MATCHER.addURI(getAuthority(), PATH_SEARCH + "/*", SEARCH_TEXT);
|
||||
@ -492,8 +492,14 @@ public class AppProvider extends FDroidProvider {
|
||||
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() {
|
||||
@ -531,9 +537,9 @@ public class AppProvider extends FDroidProvider {
|
||||
|
||||
public static Uri getRepoUri(Repo repo) {
|
||||
return getContentUri().buildUpon()
|
||||
.appendPath(PATH_REPO)
|
||||
.appendPath(String.valueOf(repo.id))
|
||||
.build();
|
||||
.appendPath(PATH_REPO)
|
||||
.appendPath(String.valueOf(repo.id))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -578,10 +584,10 @@ public class AppProvider extends FDroidProvider {
|
||||
|
||||
public static Uri getSearchUri(Repo repo, String query) {
|
||||
return getContentUri().buildUpon()
|
||||
.appendPath(PATH_SEARCH_REPO)
|
||||
.appendPath(String.valueOf(repo.id))
|
||||
.appendPath(query)
|
||||
.build();
|
||||
.appendPath(PATH_SEARCH_REPO)
|
||||
.appendPath(String.valueOf(repo.id))
|
||||
.appendPath(query)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -845,12 +851,37 @@ public class AppProvider extends FDroidProvider {
|
||||
includeSwap = false;
|
||||
break;
|
||||
|
||||
case RECENTLY_UPDATED:
|
||||
String table = getTableName();
|
||||
String isNew = table + "." + Cols.LAST_UPDATED + " <= " + table + "." + Cols.ADDED + " DESC";
|
||||
String hasFeatureGraphic = table + "." + Cols.FEATURE_GRAPHIC + " IS NULL ASC ";
|
||||
String lastUpdated = table + "." + Cols.LAST_UPDATED + " DESC";
|
||||
sortOrder = lastUpdated + ", " + isNew + ", " + hasFeatureGraphic;
|
||||
case LATEST_TAB:
|
||||
/* Sort by localized first so users see entries in their language,
|
||||
* then sort by highlighted fields, then sort by whether the app is new,
|
||||
* then if it has WhatsNew/Changelog entries, then by when it was last
|
||||
* updated. Last, it sorts by the date the app was added, putting older
|
||||
* 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
|
||||
// if it loads quickly, as it is the default view shown every time F-Droid is opened.
|
||||
@ -914,7 +945,7 @@ public class AppProvider extends FDroidProvider {
|
||||
final String app = getTableName();
|
||||
String query = "DELETE FROM " + catJoin + " WHERE " + CatJoinTable.Cols.APP_METADATA_ID + " IN " +
|
||||
"(SELECT " + Cols.ROW_ID + " FROM " + app + " WHERE " + app + "." + Cols.REPO_ID + " = ?)";
|
||||
db().execSQL(query, new String[] {String.valueOf(repoId)});
|
||||
db().execSQL(query, new String[]{String.valueOf(repoId)});
|
||||
|
||||
AppQuerySelection selection = new AppQuerySelection(where, whereArgs).add(queryRepo(repoId));
|
||||
int result = db().delete(getTableName(), selection.getSelection(), selection.getArgs());
|
||||
@ -964,7 +995,7 @@ public class AppProvider extends FDroidProvider {
|
||||
}
|
||||
|
||||
protected void ensureCategories(String[] categories, long appMetadataId) {
|
||||
db().delete(getCatJoinTableName(), CatJoinTable.Cols.APP_METADATA_ID + " = ?", new String[] {Long.toString(appMetadataId)});
|
||||
db().delete(getCatJoinTableName(), CatJoinTable.Cols.APP_METADATA_ID + " = ?", new String[]{Long.toString(appMetadataId)});
|
||||
if (categories != null) {
|
||||
Set<String> categoriesSet = new HashSet<>();
|
||||
for (String categoryName : categories) {
|
||||
@ -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
|
||||
* (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
|
||||
* 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.
|
||||
*
|
||||
* + 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,
|
||||
* but that effort should instead go into implementing an intent service.
|
||||
* <li>Compatible flags. These were calculated earlier, whether or not an app was suggested or not.</li>
|
||||
*
|
||||
* <li>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,
|
||||
* 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
|
||||
* {@link android.app.IntentService} as described in https://gitlab.com/fdroid/fdroidclient/issues/520.
|
||||
*/
|
||||
@ -1042,19 +1073,19 @@ public class AppProvider extends FDroidProvider {
|
||||
|
||||
final String highestPriority =
|
||||
"SELECT MAX(r." + RepoTable.Cols.PRIORITY + ") " +
|
||||
"FROM " + RepoTable.NAME + " AS r " +
|
||||
"JOIN " + getTableName() + " AS m ON (m." + Cols.REPO_ID + " = r." + RepoTable.Cols._ID + ") " +
|
||||
"WHERE m." + Cols.PACKAGE_ID + " = " + "metadata." + Cols.PACKAGE_ID;
|
||||
"FROM " + RepoTable.NAME + " AS r " +
|
||||
"JOIN " + getTableName() + " AS m ON (m." + Cols.REPO_ID + " = r." + RepoTable.Cols._ID + ") " +
|
||||
"WHERE m." + Cols.PACKAGE_ID + " = " + "metadata." + Cols.PACKAGE_ID;
|
||||
|
||||
String updateSql =
|
||||
"UPDATE " + PackageTable.NAME + " " +
|
||||
"SET " + PackageTable.Cols.PREFERRED_METADATA + " = ( " +
|
||||
" SELECT metadata." + Cols.ROW_ID +
|
||||
" FROM " + app + " AS metadata " +
|
||||
" JOIN " + RepoTable.NAME + " AS repo ON (metadata." + Cols.REPO_ID + " = repo." + RepoTable.Cols._ID + ") " +
|
||||
" WHERE metadata." + Cols.PACKAGE_ID + " = " + PackageTable.NAME + "." + PackageTable.Cols.ROW_ID +
|
||||
" AND repo." + RepoTable.Cols.PRIORITY + " = (" + highestPriority + ")" +
|
||||
");";
|
||||
"SET " + PackageTable.Cols.PREFERRED_METADATA + " = ( " +
|
||||
" SELECT metadata." + Cols.ROW_ID +
|
||||
" FROM " + app + " AS metadata " +
|
||||
" JOIN " + RepoTable.NAME + " AS repo ON (metadata." + Cols.REPO_ID + " = repo." + RepoTable.Cols._ID + ") " +
|
||||
" WHERE metadata." + Cols.PACKAGE_ID + " = " + PackageTable.NAME + "." + PackageTable.Cols.ROW_ID +
|
||||
" AND repo." + RepoTable.Cols.PRIORITY + " = (" + highestPriority + ")" +
|
||||
");";
|
||||
|
||||
db().execSQL(updateSql);
|
||||
}
|
||||
@ -1071,9 +1102,9 @@ public class AppProvider extends FDroidProvider {
|
||||
|
||||
String updateSql =
|
||||
"UPDATE " + app + " SET " + Cols.IS_COMPATIBLE + " = ( " +
|
||||
" SELECT TOTAL( " + apk + "." + ApkTable.Cols.IS_COMPATIBLE + ") > 0 " +
|
||||
" FROM " + apk +
|
||||
" WHERE " + apk + "." + ApkTable.Cols.APP_ID + " = " + app + "." + Cols.ROW_ID + " );";
|
||||
" SELECT TOTAL( " + apk + "." + ApkTable.Cols.IS_COMPATIBLE + ") > 0 " +
|
||||
" FROM " + apk +
|
||||
" WHERE " + apk + "." + ApkTable.Cols.APP_ID + " = " + app + "." + Cols.ROW_ID + " );";
|
||||
|
||||
db().execSQL(updateSql);
|
||||
}
|
||||
@ -1122,16 +1153,16 @@ public class AppProvider extends FDroidProvider {
|
||||
// zero rows.
|
||||
String updateSql =
|
||||
"UPDATE " + app + " SET " + Cols.AUTO_INSTALL_VERSION_CODE + " = ( " +
|
||||
" SELECT MAX( " + apk + "." + ApkTable.Cols.VERSION_CODE + " ) " +
|
||||
" FROM " + apk +
|
||||
" JOIN " + app + " AS appForThisApk ON (appForThisApk." + Cols.ROW_ID + " = " + apk + "." + ApkTable.Cols.APP_ID + ") " +
|
||||
" SELECT MAX( " + apk + "." + ApkTable.Cols.VERSION_CODE + " ) " +
|
||||
" FROM " + apk +
|
||||
" JOIN " + app + " AS appForThisApk ON (appForThisApk." + Cols.ROW_ID + " = " + apk + "." + ApkTable.Cols.APP_ID + ") " +
|
||||
" LEFT JOIN " + installed + " ON (" + installed + "." + InstalledAppTable.Cols.PACKAGE_ID + " = " + app + "." + Cols.PACKAGE_ID + ") " +
|
||||
" WHERE " +
|
||||
app + "." + Cols.PACKAGE_ID + " = appForThisApk." + Cols.PACKAGE_ID + " AND " +
|
||||
apk + "." + ApkTable.Cols.SIGNATURE + " IS COALESCE(" + installed + "." + InstalledAppTable.Cols.SIGNATURE + ", " + apk + "." + ApkTable.Cols.SIGNATURE + ") AND " +
|
||||
restrictToStable +
|
||||
" ( " + app + "." + Cols.IS_COMPATIBLE + " = 0 OR " + apk + "." + Cols.IS_COMPATIBLE + " = 1 ) ) " +
|
||||
" WHERE " + Cols.SUGGESTED_VERSION_CODE + " > 0 " + restrictToApp;
|
||||
" WHERE " +
|
||||
app + "." + Cols.PACKAGE_ID + " = appForThisApk." + Cols.PACKAGE_ID + " AND " +
|
||||
apk + "." + ApkTable.Cols.SIGNATURE + " IS COALESCE(" + installed + "." + InstalledAppTable.Cols.SIGNATURE + ", " + apk + "." + ApkTable.Cols.SIGNATURE + ") AND " +
|
||||
restrictToStable +
|
||||
" ( " + app + "." + Cols.IS_COMPATIBLE + " = 0 OR " + apk + "." + Cols.IS_COMPATIBLE + " = 1 ) ) " +
|
||||
" WHERE " + Cols.SUGGESTED_VERSION_CODE + " > 0 " + restrictToApp;
|
||||
|
||||
LoggingQuery.execSQL(db(), updateSql, args);
|
||||
}
|
||||
@ -1139,7 +1170,7 @@ public class AppProvider extends FDroidProvider {
|
||||
/**
|
||||
* We set each app's suggested version to the latest available that is
|
||||
* compatible, or the latest available if none are compatible.
|
||||
*
|
||||
* <p>
|
||||
* 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
|
||||
* algorithm as if upstreamVercode was 0.
|
||||
@ -1168,15 +1199,15 @@ public class AppProvider extends FDroidProvider {
|
||||
|
||||
String updateSql =
|
||||
"UPDATE " + app + " SET " + Cols.AUTO_INSTALL_VERSION_CODE + " = ( " +
|
||||
" SELECT MAX( " + apk + "." + ApkTable.Cols.VERSION_CODE + " ) " +
|
||||
" FROM " + apk +
|
||||
" JOIN " + app + " AS appForThisApk ON (appForThisApk." + Cols.ROW_ID + " = " + apk + "." + ApkTable.Cols.APP_ID + ") " +
|
||||
" LEFT JOIN " + installed + " ON (" + installed + "." + InstalledAppTable.Cols.PACKAGE_ID + " = " + app + "." + Cols.PACKAGE_ID + ") " +
|
||||
" WHERE " +
|
||||
app + "." + Cols.PACKAGE_ID + " = appForThisApk." + Cols.PACKAGE_ID + " AND " +
|
||||
apk + "." + ApkTable.Cols.SIGNATURE + " IS COALESCE(" + installed + "." + InstalledAppTable.Cols.SIGNATURE + ", " + apk + "." + ApkTable.Cols.SIGNATURE + ") AND " +
|
||||
" ( " + app + "." + Cols.IS_COMPATIBLE + " = 0 OR " + apk + "." + ApkTable.Cols.IS_COMPATIBLE + " = 1 ) ) " +
|
||||
" WHERE " + restrictToApps;
|
||||
" SELECT MAX( " + apk + "." + ApkTable.Cols.VERSION_CODE + " ) " +
|
||||
" FROM " + apk +
|
||||
" JOIN " + app + " AS appForThisApk ON (appForThisApk." + Cols.ROW_ID + " = " + apk + "." + ApkTable.Cols.APP_ID + ") " +
|
||||
" LEFT JOIN " + installed + " ON (" + installed + "." + InstalledAppTable.Cols.PACKAGE_ID + " = " + app + "." + Cols.PACKAGE_ID + ") " +
|
||||
" WHERE " +
|
||||
app + "." + Cols.PACKAGE_ID + " = appForThisApk." + Cols.PACKAGE_ID + " AND " +
|
||||
apk + "." + ApkTable.Cols.SIGNATURE + " IS COALESCE(" + installed + "." + InstalledAppTable.Cols.SIGNATURE + ", " + apk + "." + ApkTable.Cols.SIGNATURE + ") AND " +
|
||||
" ( " + app + "." + Cols.IS_COMPATIBLE + " = 0 OR " + apk + "." + ApkTable.Cols.IS_COMPATIBLE + " = 1 ) ) " +
|
||||
" WHERE " + restrictToApps;
|
||||
|
||||
LoggingQuery.execSQL(db(), updateSql, args);
|
||||
}
|
||||
|
@ -209,6 +209,13 @@ public interface Schema {
|
||||
String TV_SCREENSHOTS = "tvScreenshots";
|
||||
String WEAR_SCREENSHOTS = "wearScreenshots";
|
||||
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";
|
||||
|
||||
interface AutoInstallApk {
|
||||
|
@ -3,20 +3,20 @@ package org.fdroid.fdroid.views.main;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
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.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
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.R;
|
||||
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.RepoProvider;
|
||||
import org.fdroid.fdroid.data.Schema.AppMetadataTable;
|
||||
import org.fdroid.fdroid.views.apps.AppListActivity;
|
||||
import org.fdroid.fdroid.panic.HidingManager;
|
||||
import org.fdroid.fdroid.views.apps.AppListActivity;
|
||||
import org.fdroid.fdroid.views.whatsnew.WhatsNewAdapter;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see AppProvider#getLatestTabUri()
|
||||
*/
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
if (id != LOADER_ID) {
|
||||
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(
|
||||
activity,
|
||||
AppProvider.getRecentlyUpdatedUri(),
|
||||
AppProvider.getLatestTabUri(),
|
||||
AppMetadataTable.Cols.ALL,
|
||||
selection,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
@ -18,10 +18,8 @@ import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import static org.fdroid.fdroid.Assert.assertContainsOnly;
|
||||
@ -280,58 +278,6 @@ public class AppProviderTest extends FDroidProviderTest {
|
||||
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
|
||||
public void testInsertTrimsNamesAndSummary() {
|
||||
// Insert a new record with unwanted newlines...
|
||||
|
@ -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";
|
||||
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,159 @@ 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);
|
||||
}
|
||||
|
||||
@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",
|
||||
"preferredSigner",
|
||||
"prefs",
|
||||
"systemLocaleList",
|
||||
"TAG",
|
||||
};
|
||||
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