diff --git a/app/src/main/java/org/fdroid/fdroid/Utils.java b/app/src/main/java/org/fdroid/fdroid/Utils.java index 3d146d645..957746579 100644 --- a/app/src/main/java/org/fdroid/fdroid/Utils.java +++ b/app/src/main/java/org/fdroid/fdroid/Utils.java @@ -101,7 +101,7 @@ public final class Utils { // The date format used for storing dates (e.g. lastupdated, added) in the // database. - private static final SimpleDateFormat DATE_FORMAT = + public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH); private static final SimpleDateFormat TIME_FORMAT = diff --git a/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java b/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java index 389555883..04f450ea5 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java @@ -506,6 +506,9 @@ public class AppProvider extends FDroidProvider { return Uri.withAppendedPath(getContentUri(), PATH_CALC_SUGGESTED_APKS); } + /** + * Get all {@link App} entries in the given {@code category} + */ public static Uri getCategoryUri(String category) { return getContentUri().buildUpon() .appendPath(PATH_CATEGORY) @@ -519,6 +522,13 @@ public class AppProvider extends FDroidProvider { .build(); } + /** + * Get the top {@link App} entries in the given {@code category} to display + * in the overview screen in {@link org.fdroid.fdroid.views.categories.CategoryController}. + * The number of entries is defined by {@code limit}. + * + * @see org.fdroid.fdroid.views.categories.CategoryController#onCreateLoader(int, android.os.Bundle) + */ public static Uri getTopFromCategoryUri(String category, int limit) { return getContentUri().buildUpon() .appendPath(PATH_TOP_FROM_CATEGORY) @@ -842,7 +852,6 @@ public class AppProvider extends FDroidProvider { case TOP_FROM_CATEGORY: selection = selection.add(queryCategory(pathSegments.get(2))); limit = Integer.parseInt(pathSegments.get(1)); - sortOrder = getTableName() + "." + Cols.LAST_UPDATED + " DESC"; includeSwap = false; break; @@ -881,6 +890,12 @@ public class AppProvider extends FDroidProvider { /** * Helper method used by both the genuine {@link AppProvider} and the temporary version used * by the repo updater ({@link TempAppProvider}). + *

+ * Query the database table specified by {@code uri}, which is usually (always?) + * {@link AppMetadataTable} with specified {@code selection} and {@code sortOrder}. + * WARNING: This contains a hack if {@code sortOrder} is equal to {@link Cols#NAME}, + * i.e. not a complete table.column name, but just that single column name. In that case, + * a {@code sortOrder} is built out into a {@code sortOrder} that includes localized sorting. */ protected Cursor runQuery(Uri uri, AppQuerySelection selection, String[] projection, boolean includeSwap, String sortOrder, int limit) { if (!includeSwap) { diff --git a/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryController.java b/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryController.java index a0997b4cf..a0c2ffe94 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryController.java +++ b/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryController.java @@ -1,6 +1,5 @@ package org.fdroid.fdroid.views.categories; -import androidx.appcompat.app.AppCompatActivity; import android.content.Context; import android.content.Intent; import android.content.res.Resources; @@ -8,18 +7,19 @@ import android.database.Cursor; import android.graphics.Color; import android.graphics.Rect; import android.os.Bundle; -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; -import androidx.loader.app.LoaderManager; -import androidx.core.content.ContextCompat; -import androidx.loader.content.CursorLoader; -import androidx.loader.content.Loader; -import androidx.core.view.ViewCompat; -import androidx.recyclerview.widget.RecyclerView; import android.view.View; import android.widget.Button; import android.widget.FrameLayout; import android.widget.TextView; +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.ContextCompat; +import androidx.core.view.ViewCompat; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.CursorLoader; +import androidx.loader.content.Loader; +import androidx.recyclerview.widget.RecyclerView; import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer; @@ -27,6 +27,7 @@ import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.data.Schema; +import org.fdroid.fdroid.data.Schema.AppMetadataTable.Cols; import org.fdroid.fdroid.views.apps.AppListActivity; import org.fdroid.fdroid.views.apps.FeatureImage; @@ -133,9 +134,24 @@ public class CategoryController extends RecyclerView.ViewHolder implements Loade return Color.HSVToColor(hsv); } + /** + * Return either the total apps in the category, or the entries to display + * for a category, depending on the value of {@code id}. This uses a sort + * similar to the one in {@link org.fdroid.fdroid.views.main.LatestViewBinder#onCreateLoader(int, Bundle)}. + * The difference is that this does not treat "new" app any differently. + * + * @see AppProvider#getCategoryUri(String) + * @see AppProvider#getTopFromCategoryUri(String, int) + * @see AppProvider#query(android.net.Uri, String[], String, String[], String) + * @see AppProvider#TOP_FROM_CATEGORY + * @see org.fdroid.fdroid.views.main.LatestViewBinder#onCreateLoader(int, Bundle) + */ @NonNull @Override public Loader onCreateLoader(int id, Bundle args) { + final String table = Schema.AppMetadataTable.NAME; + final String added = table + "." + Cols.ADDED; + final String lastUpdated = table + "." + Cols.LAST_UPDATED; if (id == currentCategory.hashCode() + 1) { return new CursorLoader( activity, @@ -159,7 +175,22 @@ public class CategoryController extends RecyclerView.ViewHolder implements Loade }, null, null, - Schema.AppMetadataTable.Cols.NAME + 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" + + ", " + lastUpdated + " DESC" + + ", " + added + " ASC" ); } } diff --git a/app/src/test/java/org/fdroid/fdroid/data/CategoryProviderTest.java b/app/src/test/java/org/fdroid/fdroid/data/CategoryProviderTest.java index 8841f0fa7..abb625e4b 100644 --- a/app/src/test/java/org/fdroid/fdroid/data/CategoryProviderTest.java +++ b/app/src/test/java/org/fdroid/fdroid/data/CategoryProviderTest.java @@ -6,6 +6,7 @@ import android.content.ContentValues; import android.database.Cursor; import android.net.Uri; import org.fdroid.fdroid.TestUtils; +import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.Schema.AppMetadataTable.Cols; import org.fdroid.fdroid.mock.MockRepo; import org.junit.Before; @@ -163,6 +164,11 @@ public class CategoryProviderTest extends FDroidProviderTest { AppProviderTest.assertContainsOnlyIds(apps, expectedPackages); } + /** + * This does not include {@code sortOrder} since that is defined in + * {@link org.fdroid.fdroid.views.categories.CategoryController#onCreateLoader(int, android.os.Bundle)} + * so these results are sorted by the default sort. + */ @Test public void topAppsFromCategory() { insertAppWithCategory("com.dog", "Dog", "Animal", new Date(2017, 2, 6)); @@ -178,13 +184,13 @@ public class CategoryProviderTest extends FDroidProviderTest { insertAppWithCategory("com.banana", "Banana", "Vegetable", new Date(2015, 1, 1)); insertAppWithCategory("com.tomato", "Tomato", "Vegetable", new Date(2017, 4, 4)); - assertArrayEquals(getTopAppsFromCategory("Animal", 3), new String[]{"com.dog", "com.cat", "com.bird"}); - assertArrayEquals(getTopAppsFromCategory("Animal", 2), new String[]{"com.dog", "com.cat"}); - assertArrayEquals(getTopAppsFromCategory("Animal", 1), new String[]{"com.dog"}); + assertArrayEquals(new String[]{"com.bird", "com.cat", "com.dog"}, getTopAppsFromCategory("Animal", 3)); + assertArrayEquals(new String[]{"com.bird", "com.cat"}, getTopAppsFromCategory("Animal", 2)); + assertArrayEquals(new String[]{"com.bird"}, getTopAppsFromCategory("Animal", 1)); - assertArrayEquals(getTopAppsFromCategory("Mineral", 2), new String[]{"com.rock", "com.stone"}); + assertArrayEquals(new String[]{"com.boulder", "com.rock"}, getTopAppsFromCategory("Mineral", 2)); - assertArrayEquals(getTopAppsFromCategory("Vegetable", 10), new String[]{"com.tomato", "com.banana"}); + assertArrayEquals(new String[]{"com.banana", "com.tomato"}, getTopAppsFromCategory("Vegetable", 10)); } public String[] getTopAppsFromCategory(String category, int numToGet) { @@ -275,7 +281,7 @@ public class CategoryProviderTest extends FDroidProviderTest { private void insertAppWithCategory(String id, String name, String categories, Date lastUpdated, long repoId) { ContentValues values = new ContentValues(2); values.put(Cols.ForWriting.Categories.CATEGORIES, categories); - values.put(Cols.LAST_UPDATED, lastUpdated.getTime() / 1000); + values.put(Cols.LAST_UPDATED, Utils.DATE_FORMAT.format(lastUpdated)); AppProviderTest.insertApp(contentResolver, context, id, name, values, repoId); }