diff --git a/app/src/main/java/org/fdroid/fdroid/AppDetails2.java b/app/src/main/java/org/fdroid/fdroid/AppDetails2.java index 1ce4ef10c..701bd932e 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppDetails2.java +++ b/app/src/main/java/org/fdroid/fdroid/AppDetails2.java @@ -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) { diff --git a/app/src/main/java/org/fdroid/fdroid/data/App.java b/app/src/main/java/org/fdroid/fdroid/data/App.java index 65f4033b9..cedeaf062 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/App.java +++ b/app/src/main/java/org/fdroid/fdroid/data/App.java @@ -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, 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, 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, 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, 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. + *

+ * 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> localized) { // NOPMD + Locale defaultLocale = Locale.getDefault(); + String languageTag = defaultLocale.getLanguage(); + String localeTag = languageTag + "-" + defaultLocale.getCountry(); + Set locales = localized.keySet(); + Set 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> localized, + Set 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> localized, + Set 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> localized, + Set locales, String key) { + try { + for (String locale : locales) { + if (localized.containsKey(locale)) { + ArrayList entry = (ArrayList) 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 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, 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, 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, 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()); diff --git a/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java b/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java index 223a2197b..079972d5b 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java @@ -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) { diff --git a/app/src/main/java/org/fdroid/fdroid/data/Schema.java b/app/src/main/java/org/fdroid/fdroid/data/Schema.java index 6c0491fba..73d26baec 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/Schema.java +++ b/app/src/main/java/org/fdroid/fdroid/data/Schema.java @@ -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, diff --git a/app/src/main/java/org/fdroid/fdroid/views/ScreenShotsRecyclerViewAdapter.java b/app/src/main/java/org/fdroid/fdroid/views/ScreenShotsRecyclerViewAdapter.java index 08b2ac923..82034b461 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/ScreenShotsRecyclerViewAdapter.java +++ b/app/src/main/java/org/fdroid/fdroid/views/ScreenShotsRecyclerViewAdapter.java @@ -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 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