support localized text and graphics in index-v1 metadata

This sets the App instance variables using the localized index-v1 fields.
It trys to fill as many fields as possible, falling back to locales of the
same language, then finally English.

This is based on the Jackson JSON parser's ability to map a JSON key to a
method, e.g. @JsonProperty("localized")
This commit is contained in:
Hans-Christoph Steiner 2016-12-06 11:04:22 +01:00 committed by Peter Serwylo
parent d769dcfc60
commit 9d97546c4f
5 changed files with 308 additions and 8 deletions

View File

@ -136,10 +136,11 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog
recyclerView.setAdapter(adapter);
// Load the feature graphic, if present
if (!TextUtils.isEmpty(app.iconUrl)) {
String featureGraphicUrl = app.getFeatureGraphicUrl(this);
if (!TextUtils.isEmpty(featureGraphicUrl)) {
final FeatureImage featureImage = (FeatureImage) findViewById(R.id.feature_graphic);
DisplayImageOptions displayImageOptions = Utils.getImageLoadingOptions().build();
ImageLoader.getInstance().loadImage(app.iconUrl, displayImageOptions, new ImageLoadingListener() {
ImageLoader.getInstance().loadImage(featureGraphicUrl, displayImageOptions, new ImageLoadingListener() {
@Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
if (featureImage != null) {

View File

@ -17,6 +17,7 @@ import android.text.TextUtils;
import android.util.Log;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.commons.io.filefilter.RegexFileFilter;
import org.fdroid.fdroid.AppFilter;
@ -32,10 +33,16 @@ import java.io.IOException;
import java.io.InputStream;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Matcher;
@ -73,6 +80,7 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
* At most other times, we don't particularly care which repo an {@link App} object came from.
* It is pretty much transparent, because the metadata will be populated from the repo with
* the highest priority. The UI doesn't care normally _which_ repo provided the metadata.
* This is required for getting the full URL to the various graphics and screenshots.
*/
public long repoId;
public String summary = "Unknown application";
@ -80,6 +88,18 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
public String description;
public String video;
public String featureGraphic;
public String promoGraphic;
public String tvBanner;
public String[] phoneScreenshots = new String[0];
public String[] sevenInchScreenshots = new String[0];
public String[] tenInchScreenshots = new String[0];
public String[] tvScreenshots = new String[0];
public String[] wearScreenshots = new String[0];
public String license = "Unknown";
public String authorName;
@ -263,6 +283,30 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
case Cols.ICON_URL_LARGE:
iconUrlLarge = cursor.getString(i);
break;
case Cols.FEATURE_GRAPHIC:
featureGraphic = cursor.getString(i);
break;
case Cols.PROMO_GRAPHIC:
promoGraphic = cursor.getString(i);
break;
case Cols.TV_BANNER:
tvBanner = cursor.getString(i);
break;
case Cols.PHONE_SCREENSHOTS:
phoneScreenshots = Utils.parseCommaSeparatedString(cursor.getString(i));
break;
case Cols.SEVEN_INCH_SCREENSHOTS:
sevenInchScreenshots = Utils.parseCommaSeparatedString(cursor.getString(i));
break;
case Cols.TEN_INCH_SCREENSHOTS:
tenInchScreenshots = Utils.parseCommaSeparatedString(cursor.getString(i));
break;
case Cols.TV_SCREENSHOTS:
tvScreenshots = Utils.parseCommaSeparatedString(cursor.getString(i));
break;
case Cols.WEAR_SCREENSHOTS:
wearScreenshots = Utils.parseCommaSeparatedString(cursor.getString(i));
break;
case Cols.InstalledApp.VERSION_CODE:
installedVersionCode = cursor.getInt(i);
break;
@ -293,6 +337,175 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
initApkFromApkFile(context, this.installedApk, packageInfo, apkFile);
}
/**
* Parses the {@code localized} block in the incoming index metadata,
* choosing the best match in terms of locale/language while filling as
* many fields as possible. The first English locale found is loaded, then
* {@code en-US} is loaded over that, since that's the most common English
* for software. Then the first language match, and then finally the
* current locale for this device, given it precedence over all the others.
* <p>
* It is still possible that the fields will be loaded directly without any
* locale info. This comes from the old-style {@code .txt} app metadata
* fields that do not have locale info. They should not be used if the
* {@code Localized} block is specified.
*/
@JsonProperty("localized")
private void setLocalized(Map<String, Map<String, Object>> localized) { // NOPMD
Locale defaultLocale = Locale.getDefault();
String languageTag = defaultLocale.getLanguage();
String localeTag = languageTag + "-" + defaultLocale.getCountry();
Set<String> locales = localized.keySet();
Set<String> localesToUse = new TreeSet<>();
if (locales.contains(localeTag)) {
localesToUse.add(localeTag);
}
for (String l : locales) {
if (l.startsWith(languageTag)) {
localesToUse.add(l);
break;
}
}
if (locales.contains("en-US")) {
localesToUse.add("en-US");
}
for (String l : locales) {
if (l.startsWith("en")) {
localesToUse.add(l);
break;
}
}
// if key starts with Upper case, its set by humans
// Name, Summary, Description existed before localization so their values can be set directly
video = getLocalizedEntry(localized, localesToUse, "Video");
String value = getLocalizedEntry(localized, localesToUse, "Name");
if (!TextUtils.isEmpty(value)) {
name = value;
}
value = getLocalizedEntry(localized, localesToUse, "Summary");
if (!TextUtils.isEmpty(value)) {
summary = value;
}
description = getLocalizedEntry(localized, localesToUse, "Description");
if (!TextUtils.isEmpty(value)) {
description = value;
}
// if key starts with lower case, its generated based on finding the files
featureGraphic = getLocalizedGraphicsEntry(localized, localesToUse, "featureGraphic");
promoGraphic = getLocalizedGraphicsEntry(localized, localesToUse, "promoGraphic");
tvBanner = getLocalizedGraphicsEntry(localized, localesToUse, "tvBanner");
wearScreenshots = setLocalizedListEntry(localized, localesToUse, "wearScreenshots");
phoneScreenshots = setLocalizedListEntry(localized, localesToUse, "phoneScreenshots");
sevenInchScreenshots = setLocalizedListEntry(localized, localesToUse, "sevenInchScreenshots");
tenInchScreenshots = setLocalizedListEntry(localized, localesToUse, "tenInchScreenshots");
tvScreenshots = setLocalizedListEntry(localized, localesToUse, "tvScreenshots");
}
private String getLocalizedEntry(Map<String, Map<String, Object>> localized,
Set<String> locales, String key) {
try {
for (String locale : locales) {
if (localized.containsKey(locale)) {
return (String) localized.get(locale).get(key);
}
}
} catch (ClassCastException e) {
Utils.debugLog(TAG, e.getMessage());
}
return null;
}
private String getLocalizedGraphicsEntry(Map<String, Map<String, Object>> localized,
Set<String> locales, String key) {
try {
for (String locale : locales) {
if (localized.containsKey(locale)) {
return locale + "/" + localized.get(locale).get(key);
}
}
} catch (ClassCastException e) {
Utils.debugLog(TAG, e.getMessage());
}
return null;
}
private String[] setLocalizedListEntry(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++;
}
return result;
}
}
}
} catch (ClassCastException e) {
Utils.debugLog(TAG, e.getMessage());
}
return new String[0];
}
public String getFeatureGraphicUrl(Context context) {
if (TextUtils.isEmpty(featureGraphic)) {
return null;
}
Repo repo = RepoProvider.Helper.findById(context, repoId);
return repo.address + "/" + packageName + "/" + featureGraphic;
}
public String getPromoGraphic(Context context) {
if (TextUtils.isEmpty(promoGraphic)) {
return null;
}
Repo repo = RepoProvider.Helper.findById(context, repoId);
return repo.address + "/" + packageName + "/" + promoGraphic;
}
public String getTvBanner(Context context) {
if (TextUtils.isEmpty(tvBanner)) {
return null;
}
Repo repo = RepoProvider.Helper.findById(context, repoId);
return repo.address + "/" + packageName + "/" + tvBanner;
}
public String[] getAllScreenshots(Context context) {
Repo repo = RepoProvider.Helper.findById(context, repoId);
ArrayList<String> list = new ArrayList<>();
if (phoneScreenshots != null) {
Collections.addAll(list, phoneScreenshots);
}
if (sevenInchScreenshots != null) {
Collections.addAll(list, sevenInchScreenshots);
}
if (tenInchScreenshots != null) {
Collections.addAll(list, tenInchScreenshots);
}
if (tvScreenshots != null) {
Collections.addAll(list, tvScreenshots);
}
if (wearScreenshots != null) {
Collections.addAll(list, wearScreenshots);
}
String[] result = new String[list.size()];
int i = 0;
for (String url : list) {
result[i] = repo.address + "/" + packageName + "/" + url;
i++;
}
return result;
}
/**
* Get the directory where APK Expansion Files aka OBB files are stored for the app as
* specified by {@code packageName}.
@ -525,6 +738,14 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
values.put(Cols.ForWriting.Categories.CATEGORIES, Utils.serializeCommaSeparatedString(categories));
values.put(Cols.ANTI_FEATURES, Utils.serializeCommaSeparatedString(antiFeatures));
values.put(Cols.REQUIREMENTS, Utils.serializeCommaSeparatedString(requirements));
values.put(Cols.FEATURE_GRAPHIC, featureGraphic);
values.put(Cols.PROMO_GRAPHIC, promoGraphic);
values.put(Cols.TV_BANNER, tvBanner);
values.put(Cols.PHONE_SCREENSHOTS, Utils.serializeCommaSeparatedString(phoneScreenshots));
values.put(Cols.SEVEN_INCH_SCREENSHOTS, Utils.serializeCommaSeparatedString(sevenInchScreenshots));
values.put(Cols.TEN_INCH_SCREENSHOTS, Utils.serializeCommaSeparatedString(tenInchScreenshots));
values.put(Cols.TV_SCREENSHOTS, Utils.serializeCommaSeparatedString(tvScreenshots));
values.put(Cols.WEAR_SCREENSHOTS, Utils.serializeCommaSeparatedString(wearScreenshots));
values.put(Cols.IS_COMPATIBLE, compatible ? 1 : 0);
return values;
@ -674,6 +895,14 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
dest.writeStringArray(this.requirements);
dest.writeString(this.iconUrl);
dest.writeString(this.iconUrlLarge);
dest.writeString(this.featureGraphic);
dest.writeString(this.promoGraphic);
dest.writeString(this.tvBanner);
dest.writeStringArray(this.phoneScreenshots);
dest.writeStringArray(this.sevenInchScreenshots);
dest.writeStringArray(this.tenInchScreenshots);
dest.writeStringArray(this.tvScreenshots);
dest.writeStringArray(this.wearScreenshots);
dest.writeString(this.installedVersionName);
dest.writeInt(this.installedVersionCode);
dest.writeParcelable(this.installedApk, flags);
@ -713,6 +942,14 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
this.requirements = in.createStringArray();
this.iconUrl = in.readString();
this.iconUrlLarge = in.readString();
this.featureGraphic = in.readString();
this.promoGraphic = in.readString();
this.tvBanner = in.readString();
this.phoneScreenshots = in.createStringArray();
this.sevenInchScreenshots = in.createStringArray();
this.tenInchScreenshots = in.createStringArray();
this.tvScreenshots = in.createStringArray();
this.wearScreenshots = in.createStringArray();
this.installedVersionName = in.readString();
this.installedVersionCode = in.readInt();
this.installedApk = in.readParcelable(Apk.class.getClassLoader());

View File

@ -140,6 +140,14 @@ class DBHelper extends SQLiteOpenHelper {
+ AppMetadataTable.Cols.IS_COMPATIBLE + " int not null,"
+ AppMetadataTable.Cols.ICON_URL + " text, "
+ AppMetadataTable.Cols.ICON_URL_LARGE + " text, "
+ AppMetadataTable.Cols.FEATURE_GRAPHIC + " string,"
+ AppMetadataTable.Cols.PROMO_GRAPHIC + " string,"
+ AppMetadataTable.Cols.TV_BANNER + " string,"
+ AppMetadataTable.Cols.PHONE_SCREENSHOTS + " string,"
+ AppMetadataTable.Cols.SEVEN_INCH_SCREENSHOTS + " string,"
+ AppMetadataTable.Cols.TEN_INCH_SCREENSHOTS + " string,"
+ AppMetadataTable.Cols.TV_SCREENSHOTS + " string,"
+ AppMetadataTable.Cols.WEAR_SCREENSHOTS + " string,"
+ "primary key(" + AppMetadataTable.Cols.PACKAGE_ID + ", " + AppMetadataTable.Cols.REPO_ID + "));";
private static final String CREATE_TABLE_APP_PREFS = "CREATE TABLE " + AppPrefsTable.NAME
@ -182,7 +190,7 @@ class DBHelper extends SQLiteOpenHelper {
+ InstalledAppTable.Cols.HASH + " TEXT NOT NULL"
+ " );";
protected static final int DB_VERSION = 66;
protected static final int DB_VERSION = 67;
private final Context context;
@ -263,6 +271,47 @@ class DBHelper extends SQLiteOpenHelper {
addObbFiles(db, oldVersion);
addCategoryTables(db, oldVersion);
addIndexV1Fields(db, oldVersion);
addIndexV1AppFields(db, oldVersion);
}
private void addIndexV1AppFields(SQLiteDatabase db, int oldVersion) {
if (oldVersion >= 67) {
return;
}
// Strings
if (!columnExists(db, AppMetadataTable.NAME, AppMetadataTable.Cols.FEATURE_GRAPHIC)) {
Utils.debugLog(TAG, "Adding " + AppMetadataTable.Cols.FEATURE_GRAPHIC + " field to " + AppMetadataTable.NAME + " table in db.");
db.execSQL("alter table " + AppMetadataTable.NAME + " add column " + AppMetadataTable.Cols.FEATURE_GRAPHIC + " string;");
}
if (!columnExists(db, AppMetadataTable.NAME, AppMetadataTable.Cols.PROMO_GRAPHIC)) {
Utils.debugLog(TAG, "Adding " + AppMetadataTable.Cols.PROMO_GRAPHIC + " field to " + AppMetadataTable.NAME + " table in db.");
db.execSQL("alter table " + AppMetadataTable.NAME + " add column " + AppMetadataTable.Cols.PROMO_GRAPHIC + " string;");
}
if (!columnExists(db, AppMetadataTable.NAME, AppMetadataTable.Cols.TV_BANNER)) {
Utils.debugLog(TAG, "Adding " + AppMetadataTable.Cols.TV_BANNER + " field to " + AppMetadataTable.NAME + " table in db.");
db.execSQL("alter table " + AppMetadataTable.NAME + " add column " + AppMetadataTable.Cols.TV_BANNER + " string;");
}
// String Arrays
if (!columnExists(db, AppMetadataTable.NAME, AppMetadataTable.Cols.PHONE_SCREENSHOTS)) {
Utils.debugLog(TAG, "Adding " + AppMetadataTable.Cols.PHONE_SCREENSHOTS + " field to " + AppMetadataTable.NAME + " table in db.");
db.execSQL("alter table " + AppMetadataTable.NAME + " add column " + AppMetadataTable.Cols.PHONE_SCREENSHOTS + " string;");
}
if (!columnExists(db, AppMetadataTable.NAME, AppMetadataTable.Cols.SEVEN_INCH_SCREENSHOTS)) {
Utils.debugLog(TAG, "Adding " + AppMetadataTable.Cols.SEVEN_INCH_SCREENSHOTS + " field to " + AppMetadataTable.NAME + " table in db.");
db.execSQL("alter table " + AppMetadataTable.NAME + " add column " + AppMetadataTable.Cols.SEVEN_INCH_SCREENSHOTS + " string;");
}
if (!columnExists(db, AppMetadataTable.NAME, AppMetadataTable.Cols.TEN_INCH_SCREENSHOTS)) {
Utils.debugLog(TAG, "Adding " + AppMetadataTable.Cols.TEN_INCH_SCREENSHOTS + " field to " + AppMetadataTable.NAME + " table in db.");
db.execSQL("alter table " + AppMetadataTable.NAME + " add column " + AppMetadataTable.Cols.TEN_INCH_SCREENSHOTS + " string;");
}
if (!columnExists(db, AppMetadataTable.NAME, AppMetadataTable.Cols.TV_SCREENSHOTS)) {
Utils.debugLog(TAG, "Adding " + AppMetadataTable.Cols.TV_SCREENSHOTS + " field to " + AppMetadataTable.NAME + " table in db.");
db.execSQL("alter table " + AppMetadataTable.NAME + " add column " + AppMetadataTable.Cols.TV_SCREENSHOTS + " string;");
}
if (!columnExists(db, AppMetadataTable.NAME, AppMetadataTable.Cols.WEAR_SCREENSHOTS)) {
Utils.debugLog(TAG, "Adding " + AppMetadataTable.Cols.WEAR_SCREENSHOTS + " field to " + AppMetadataTable.NAME + " table in db.");
db.execSQL("alter table " + AppMetadataTable.NAME + " add column " + AppMetadataTable.Cols.WEAR_SCREENSHOTS + " string;");
}
}
private void addIndexV1Fields(SQLiteDatabase db, int oldVersion) {

View File

@ -143,6 +143,14 @@ public interface Schema {
String REQUIREMENTS = "requirements";
String ICON_URL = "iconUrl";
String ICON_URL_LARGE = "iconUrlLarge";
String FEATURE_GRAPHIC = "featureGraphic";
String PROMO_GRAPHIC = "promoGraphic";
String TV_BANNER = "tvBanner";
String PHONE_SCREENSHOTS = "phoneScreenshots";
String SEVEN_INCH_SCREENSHOTS = "sevenInchScreenshots";
String TEN_INCH_SCREENSHOTS = "tenInchScreenshots";
String TV_SCREENSHOTS = "tvScreenshots";
String WEAR_SCREENSHOTS = "wearScreenshots";
interface SuggestedApk {
String VERSION_NAME = "suggestedApkVersion";
@ -180,6 +188,8 @@ public interface Schema {
CHANGELOG, DONATE, BITCOIN, LITECOIN, FLATTR_ID,
UPSTREAM_VERSION_NAME, UPSTREAM_VERSION_CODE, ADDED, LAST_UPDATED,
ANTI_FEATURES, REQUIREMENTS, ICON_URL, ICON_URL_LARGE,
FEATURE_GRAPHIC, PROMO_GRAPHIC, TV_BANNER, PHONE_SCREENSHOTS,
SEVEN_INCH_SCREENSHOTS, TEN_INCH_SCREENSHOTS, TV_SCREENSHOTS, WEAR_SCREENSHOTS,
SUGGESTED_VERSION_CODE,
};
@ -194,6 +204,8 @@ public interface Schema {
CHANGELOG, DONATE, BITCOIN, LITECOIN, FLATTR_ID,
UPSTREAM_VERSION_NAME, UPSTREAM_VERSION_CODE, ADDED, LAST_UPDATED,
ANTI_FEATURES, REQUIREMENTS, ICON_URL, ICON_URL_LARGE,
FEATURE_GRAPHIC, PROMO_GRAPHIC, TV_BANNER, PHONE_SCREENSHOTS,
SEVEN_INCH_SCREENSHOTS, TEN_INCH_SCREENSHOTS, TV_SCREENSHOTS, WEAR_SCREENSHOTS,
SUGGESTED_VERSION_CODE, SuggestedApk.VERSION_NAME,
InstalledApp.VERSION_CODE, InstalledApp.VERSION_NAME,
InstalledApp.SIGNATURE, Package.PACKAGE_NAME,

View File

@ -12,20 +12,21 @@ import android.widget.ImageView;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
import com.nostra13.universalimageloader.core.download.ImageDownloader;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.App;
public class ScreenShotsRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements LinearLayoutManagerSnapHelper.LinearSnapHelperListener {
private final String[] screenshots;
private final DisplayImageOptions displayImageOptions;
private View selectedView;
private int selectedPosition;
private final int selectedItemElevation;
private final int unselectedItemMargin;
public ScreenShotsRecyclerViewAdapter(Context context, @SuppressWarnings("unused") App app) {
public ScreenShotsRecyclerViewAdapter(Context context, App app) {
super();
screenshots = app.getAllScreenshots(context);
selectedPosition = 0;
selectedItemElevation = context.getResources().getDimensionPixelSize(R.dimen.details_screenshot_selected_elevation);
unselectedItemMargin = context.getResources().getDimensionPixelSize(R.dimen.details_screenshot_margin);
@ -46,8 +47,8 @@ public class ScreenShotsRecyclerViewAdapter extends RecyclerView.Adapter<Recycle
if (position == selectedPosition) {
this.selectedView = vh.itemView;
}
// For now, use the screenshot placeholder
ImageLoader.getInstance().displayImage(ImageDownloader.Scheme.ASSETS.wrap("screenshot_placeholder.png"), vh.image, displayImageOptions);
ImageLoader.getInstance().displayImage(screenshots[position],
vh.image, displayImageOptions);
}
@Override
@ -59,7 +60,7 @@ public class ScreenShotsRecyclerViewAdapter extends RecyclerView.Adapter<Recycle
@Override
public int getItemCount() {
return 7;
return screenshots.length;
}
@Override