prefer complete, localized apps in the category overview cards

To further the goal of providing a fully localized experience based on the
user's Language Settings, this applies similar logic as the Latest Tab to
the apps that are featured for each category.
This commit is contained in:
Hans-Christoph Steiner 2021-02-09 12:15:35 +01:00
parent a0c809f0ad
commit 22cffbb0d8
4 changed files with 70 additions and 18 deletions

View File

@ -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 =

View File

@ -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}).
* <p>
* Query the database table specified by {@code uri}, which is usually (always?)
* {@link AppMetadataTable} with specified {@code selection} and {@code sortOrder}.
* <b>WARNING:</b> 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) {

View File

@ -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<Cursor> 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"
);
}
}

View File

@ -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);
}