From 8856f19277151292c4ab1be02471f3c72cb06c3d Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Mon, 8 Feb 2021 14:30:33 +0100 Subject: [PATCH 01/11] move Latest Tab sort closer to where it is actually used --- .../org/fdroid/fdroid/data/AppProvider.java | 31 ----------------- .../fdroid/views/main/LatestViewBinder.java | 33 +++++++++++++++++-- 2 files changed, 31 insertions(+), 33 deletions(-) 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 2751788c7..910ab70c2 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java @@ -852,37 +852,6 @@ public class AppProvider extends FDroidProvider { break; 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. // 200 is an arbitrary number which hopefully gives the user enough to scroll through diff --git a/app/src/main/java/org/fdroid/fdroid/views/main/LatestViewBinder.java b/app/src/main/java/org/fdroid/fdroid/views/main/LatestViewBinder.java index dee85b698..a53315ce6 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/main/LatestViewBinder.java +++ b/app/src/main/java/org/fdroid/fdroid/views/main/LatestViewBinder.java @@ -24,6 +24,7 @@ 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.data.Schema.AppMetadataTable.Cols; import org.fdroid.fdroid.panic.HidingManager; import org.fdroid.fdroid.views.apps.AppListActivity; @@ -94,6 +95,13 @@ class LatestViewBinder implements LoaderManager.LoaderCallbacks { } /** + * 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. + * * @see AppProvider#getLatestTabUri() */ @NonNull @@ -102,14 +110,35 @@ class LatestViewBinder implements LoaderManager.LoaderCallbacks { if (id != LOADER_ID) { return null; } + final String table = AppMetadataTable.NAME; + final String added = table + "." + Cols.ADDED; + final String lastUpdated = table + "." + Cols.LAST_UPDATED; return new CursorLoader( activity, AppProvider.getLatestTabUri(), AppMetadataTable.Cols.ALL, null, null, - null - ); + 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"); } @Override From 9c3176852e325f2c08f5a7c2f236d7ffc597b5db Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Mon, 8 Feb 2021 14:47:50 +0100 Subject: [PATCH 02/11] more descriptive variable name --- .../fdroid/fdroid/views/categories/CategoryController.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 98391dead..a0997b4cf 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 @@ -43,7 +43,7 @@ public class CategoryController extends RecyclerView.ViewHolder implements Loade private final AppCompatActivity activity; private final LoaderManager loaderManager; private final DisplayImageOptions displayImageOptions; - private static int categoryItemCount = 20; + private static final int NUM_OF_APPS_PER_CATEGORY_ON_OVERVIEW = 20; private String currentCategory; @@ -148,7 +148,7 @@ public class CategoryController extends RecyclerView.ViewHolder implements Loade } else { return new CursorLoader( activity, - AppProvider.getTopFromCategoryUri(currentCategory, categoryItemCount), + AppProvider.getTopFromCategoryUri(currentCategory, NUM_OF_APPS_PER_CATEGORY_ON_OVERVIEW), new String[]{ Schema.AppMetadataTable.Cols.NAME, Schema.AppMetadataTable.Cols.Package.PACKAGE_NAME, @@ -231,7 +231,7 @@ public class CategoryController extends RecyclerView.ViewHolder implements Loade boolean isLtr = ViewCompat.getLayoutDirection(parent) == ViewCompat.LAYOUT_DIRECTION_LTR; int itemPosition = parent.getChildLayoutPosition(view); boolean first = itemPosition == 0; - boolean end = itemPosition == categoryItemCount - 1; + boolean end = itemPosition == NUM_OF_APPS_PER_CATEGORY_ON_OVERVIEW - 1; // Leave this "paddingEnd" local variable here for clarity when converting from // left/right to start/end for RTL friendly layout. From 646f2c8e9fbaddd26f173226f1217b8118fb557f Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Mon, 8 Feb 2021 16:17:19 +0100 Subject: [PATCH 03/11] overhaul search to include summary, better searching, and localized sort include @gcbrown76's `getSortOrder()` from !889 as getLastUpdatedSortOrder --- .../fdroid/views/apps/AppListActivity.java | 71 ++++++++++++++----- 1 file changed, 53 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/views/apps/AppListActivity.java b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListActivity.java index 78664ff87..51fd9545d 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/apps/AppListActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListActivity.java @@ -32,7 +32,6 @@ import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.ImageView; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; @@ -43,14 +42,13 @@ import androidx.loader.content.CursorLoader; import androidx.loader.content.Loader; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; - import com.nostra13.universalimageloader.core.ImageLoader; - import org.fdroid.fdroid.FDroidApp; 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; +import org.fdroid.fdroid.data.Schema.AppMetadataTable.Cols; /** * Provides scrollable listing of apps for search and category views. @@ -58,6 +56,8 @@ import org.fdroid.fdroid.data.Schema; public class AppListActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks, CategoryTextWatcher.SearchTermsChangedListener { + public static final String TAG = "AppListActivity"; + public static final String EXTRA_CATEGORY = "org.fdroid.fdroid.views.apps.AppListActivity.EXTRA_CATEGORY"; public static final String EXTRA_SEARCH_TERMS @@ -74,9 +74,8 @@ public class AppListActivity extends AppCompatActivity implements LoaderManager. private Utils.KeyboardStateMonitor keyboardStateMonitor; private interface SortClause { - String NAME = Schema.AppMetadataTable.NAME + "." + Schema.AppMetadataTable.Cols.NAME + " asc"; - String LAST_UPDATED = Schema.AppMetadataTable.NAME + "." - + Schema.AppMetadataTable.Cols.LAST_UPDATED + " desc"; + String NAME = Cols.NAME; + String LAST_UPDATED = Cols.LAST_UPDATED; } @Override @@ -115,15 +114,19 @@ public class AppListActivity extends AppCompatActivity implements LoaderManager. sortImage.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - if (sortClauseSelected.equalsIgnoreCase(SortClause.LAST_UPDATED)) { - sortClauseSelected = SortClause.NAME; - final Drawable alphabetical = DrawableCompat.wrap( - ContextCompat.getDrawable(AppListActivity.this, R.drawable.ic_sort_by_alpha)).mutate(); - DrawableCompat.setTint(alphabetical, FDroidApp.isAppThemeLight() ? Color.BLACK : Color.WHITE); - sortImage.setImageDrawable(alphabetical); - } else { - sortClauseSelected = SortClause.LAST_UPDATED; - sortImage.setImageDrawable(lastUpdated); + switch (sortClauseSelected) { + case SortClause.NAME: + sortClauseSelected = SortClause.LAST_UPDATED; + sortImage.setImageDrawable(lastUpdated); + break; + case SortClause.LAST_UPDATED: + sortClauseSelected = SortClause.NAME; + final Drawable alphabetical = DrawableCompat.wrap( + ContextCompat.getDrawable(AppListActivity.this, R.drawable.ic_sort_by_alpha)) + .mutate(); + DrawableCompat.setTint(alphabetical, FDroidApp.isAppThemeLight() ? Color.BLACK : Color.WHITE); + sortImage.setImageDrawable(alphabetical); + break; } getSupportLoaderManager().restartLoader(0, null, AppListActivity.this); appView.scrollToPosition(0); @@ -218,10 +221,10 @@ public class AppListActivity extends AppCompatActivity implements LoaderManager. return new CursorLoader( this, AppProvider.getSearchUri(searchTerms, category), - Schema.AppMetadataTable.Cols.ALL, + AppMetadataTable.Cols.ALL, null, null, - sortClauseSelected + getSortOrder() ); } @@ -248,4 +251,36 @@ public class AppListActivity extends AppCompatActivity implements LoaderManager. this.searchTerms = searchTerms; getSupportLoaderManager().restartLoader(0, null, this); } + + private String getSortOrder() { + final String nameCol = AppMetadataTable.NAME + "." + AppMetadataTable.Cols.NAME; + final String summaryCol = AppMetadataTable.NAME + "." + AppMetadataTable.Cols.SUMMARY; + final String nameSort = AppMetadataTable.NAME + "." + Cols.NAME + " COLLATE LOCALIZED "; + final String lastUpdatedSort = AppMetadataTable.NAME + "." + Cols.LAST_UPDATED + " DESC"; + String sortOrder; + switch (sortClauseSelected) { + case SortClause.NAME: + sortOrder = nameSort; + break; + case SortClause.LAST_UPDATED: + sortOrder = lastUpdatedSort; + break; + default: + sortOrder = nameSort; + } + + final String[] terms = searchTerms.trim().split("\\s+"); + if (terms.length == 0 || terms[0].equals("")) { + return sortOrder; + } + StringBuilder titleCase = new StringBuilder(String.format("%s like '%%%s%%'", nameCol, terms[0])); + StringBuilder summaryCase = new StringBuilder(String.format("%s like '%%%s%%'", summaryCol, terms[0])); + for (int i = 1; i < terms.length; i++) { + titleCase.append(String.format(" and %s like '%%%s%%'", nameCol, terms[i])); + summaryCase.append(String.format(" and %s like '%%%s%%'", summaryCol, terms[i])); + } + + return String.format("case when %s then 1 when %s then 2 else 3 end, %s", + titleCase.toString(), summaryCase.toString(), sortOrder); + } } From c3c31ed0338ba0d1403a59b8813e2ff28afca778 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Mon, 8 Feb 2021 22:04:24 +0100 Subject: [PATCH 04/11] convert alpha sort into "sort by search terms", keep "last updated" refs #1600 closes #1522 closes #1185 --- .../fdroid/views/apps/AppListActivity.java | 71 +++++++++++++------ app/src/main/res/drawable/ic_sort.xml | 10 +++ .../main/res/drawable/ic_sort_by_alpha.xml | 10 --- 3 files changed, 60 insertions(+), 31 deletions(-) create mode 100644 app/src/main/res/drawable/ic_sort.xml delete mode 100644 app/src/main/res/drawable/ic_sort_by_alpha.xml diff --git a/app/src/main/java/org/fdroid/fdroid/views/apps/AppListActivity.java b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListActivity.java index 51fd9545d..9007c1606 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/apps/AppListActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListActivity.java @@ -74,7 +74,7 @@ public class AppListActivity extends AppCompatActivity implements LoaderManager. private Utils.KeyboardStateMonitor keyboardStateMonitor; private interface SortClause { - String NAME = Cols.NAME; + String WORDS = Cols.NAME; String LAST_UPDATED = Cols.LAST_UPDATED; } @@ -115,14 +115,14 @@ public class AppListActivity extends AppCompatActivity implements LoaderManager. @Override public void onClick(View view) { switch (sortClauseSelected) { - case SortClause.NAME: + case SortClause.WORDS: sortClauseSelected = SortClause.LAST_UPDATED; sortImage.setImageDrawable(lastUpdated); break; case SortClause.LAST_UPDATED: - sortClauseSelected = SortClause.NAME; + sortClauseSelected = SortClause.WORDS; final Drawable alphabetical = DrawableCompat.wrap( - ContextCompat.getDrawable(AppListActivity.this, R.drawable.ic_sort_by_alpha)) + ContextCompat.getDrawable(AppListActivity.this, R.drawable.ic_sort)) .mutate(); DrawableCompat.setTint(alphabetical, FDroidApp.isAppThemeLight() ? Color.BLACK : Color.WHITE); sortImage.setImageDrawable(alphabetical); @@ -253,25 +253,36 @@ public class AppListActivity extends AppCompatActivity implements LoaderManager. } private String getSortOrder() { - final String nameCol = AppMetadataTable.NAME + "." + AppMetadataTable.Cols.NAME; - final String summaryCol = AppMetadataTable.NAME + "." + AppMetadataTable.Cols.SUMMARY; - final String nameSort = AppMetadataTable.NAME + "." + Cols.NAME + " COLLATE LOCALIZED "; - final String lastUpdatedSort = AppMetadataTable.NAME + "." + Cols.LAST_UPDATED + " DESC"; - String sortOrder; - switch (sortClauseSelected) { - case SortClause.NAME: - sortOrder = nameSort; - break; - case SortClause.LAST_UPDATED: - sortOrder = lastUpdatedSort; - break; - default: - sortOrder = nameSort; + final String table = AppMetadataTable.NAME; + final String nameCol = table + "." + AppMetadataTable.Cols.NAME; + final String summaryCol = table + "." + AppMetadataTable.Cols.SUMMARY; + final String packageCol = Cols.Package.PACKAGE_NAME; + + if (sortClauseSelected.equals(SortClause.LAST_UPDATED)) { + return table + "." + Cols.LAST_UPDATED + " DESC" + + ", " + table + "." + Cols.IS_LOCALIZED + " DESC" + + ", " + table + "." + Cols.ADDED + " ASC" + + ", " + table + "." + Cols.NAME + " IS NULL ASC" + + ", " + table + "." + Cols.ICON + " IS NULL ASC" + + ", " + table + "." + Cols.SUMMARY + " IS NULL ASC" + + ", " + table + "." + Cols.DESCRIPTION + " IS NULL ASC" + + ", " + table + "." + Cols.WHATSNEW + " 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"; } - final String[] terms = searchTerms.trim().split("\\s+"); + // prevent SQL injection https://en.wikipedia.org/wiki/SQL_injection#Escaping + final String[] terms = searchTerms.trim().replaceAll("[\\x1a\0\n\r\"';\\\\]+", " ").split("\\s+"); if (terms.length == 0 || terms[0].equals("")) { - return sortOrder; + return table + "." + Cols.NAME + " COLLATE LOCALIZED "; + } } StringBuilder titleCase = new StringBuilder(String.format("%s like '%%%s%%'", nameCol, terms[0])); StringBuilder summaryCase = new StringBuilder(String.format("%s like '%%%s%%'", summaryCol, terms[0])); @@ -281,6 +292,24 @@ public class AppListActivity extends AppCompatActivity implements LoaderManager. } return String.format("case when %s then 1 when %s then 2 else 3 end, %s", - titleCase.toString(), summaryCase.toString(), sortOrder); + titleCase.toString(), summaryCase.toString(), "" + + ", " + table + "." + Cols.IS_LOCALIZED + " DESC" + + ", " + table + "." + Cols.ADDED + " ASC" + + ", " + table + "." + Cols.NAME + " IS NULL ASC" + + ", " + table + "." + Cols.ICON + " IS NULL ASC" + + ", " + table + "." + Cols.SUMMARY + " IS NULL ASC" + + ", " + table + "." + Cols.DESCRIPTION + " IS NULL ASC" + + ", " + table + "." + Cols.WHATSNEW + " 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" + + ", " + table + "." + Cols.LAST_UPDATED + " DESC" + ); } } diff --git a/app/src/main/res/drawable/ic_sort.xml b/app/src/main/res/drawable/ic_sort.xml new file mode 100644 index 000000000..69d39e915 --- /dev/null +++ b/app/src/main/res/drawable/ic_sort.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_sort_by_alpha.xml b/app/src/main/res/drawable/ic_sort_by_alpha.xml deleted file mode 100644 index cecad4568..000000000 --- a/app/src/main/res/drawable/ic_sort_by_alpha.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - From 1c8a56e314b96e538376495139fd4950f92f7501 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Mon, 8 Feb 2021 22:53:51 +0100 Subject: [PATCH 05/11] if search are letters with a "." in the middle, enable packageName This makes it easy to search by Application ID aka Package Name without affecting searches that definitely cannot be a Package Name. --- .../fdroid/views/apps/AppListActivity.java | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/views/apps/AppListActivity.java b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListActivity.java index 9007c1606..32594d952 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/apps/AppListActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListActivity.java @@ -283,16 +283,33 @@ public class AppListActivity extends AppCompatActivity implements LoaderManager. if (terms.length == 0 || terms[0].equals("")) { return table + "." + Cols.NAME + " COLLATE LOCALIZED "; } + + boolean potentialPackageName = false; + StringBuilder packageNameFirstCase = new StringBuilder(); + if (terms[0].length() > 2 && terms[0].substring(1, terms[0].length() - 1).contains(".")) { + potentialPackageName = true; + packageNameFirstCase.append(String.format("%s LIKE '%%%s%%' ", + packageCol, terms[0])); } StringBuilder titleCase = new StringBuilder(String.format("%s like '%%%s%%'", nameCol, terms[0])); StringBuilder summaryCase = new StringBuilder(String.format("%s like '%%%s%%'", summaryCol, terms[0])); + StringBuilder packageNameCase = new StringBuilder(String.format("%s like '%%%s%%'", packageCol, terms[0])); for (int i = 1; i < terms.length; i++) { + if (potentialPackageName) { + packageNameCase.append(String.format(" and %s like '%%%s%%'", summaryCol, terms[i])); + } titleCase.append(String.format(" and %s like '%%%s%%'", nameCol, terms[i])); summaryCase.append(String.format(" and %s like '%%%s%%'", summaryCol, terms[i])); } - - return String.format("case when %s then 1 when %s then 2 else 3 end, %s", - titleCase.toString(), summaryCase.toString(), "" + String sortOrder; + if (packageNameCase.length() > 0) { + sortOrder = String.format("CASE WHEN %s THEN 0 WHEN %s THEN 1 WHEN %s THEN 2 ELSE 3 END", + packageNameCase.toString(), titleCase.toString(), summaryCase.toString()); + } else { + sortOrder = String.format("CASE WHEN %s THEN 1 WHEN %s THEN 2 ELSE 3 END", + titleCase.toString(), summaryCase.toString()); + } + return sortOrder + ", " + table + "." + Cols.IS_LOCALIZED + " DESC" + ", " + table + "." + Cols.ADDED + " ASC" + ", " + table + "." + Cols.NAME + " IS NULL ASC" @@ -309,7 +326,6 @@ public class AppListActivity extends AppCompatActivity implements LoaderManager. + " AND " + table + "." + Cols.PROMO_GRAPHIC + " IS NULL" + " AND " + table + "." + Cols.TV_BANNER + " IS NULL" + " THEN 1 ELSE 0 END" - + ", " + table + "." + Cols.LAST_UPDATED + " DESC" - ); + + ", " + table + "." + Cols.LAST_UPDATED + " DESC"; } } From f38450fc1966a94df42dd87e5a3cc9f604e263ab Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Mon, 8 Feb 2021 23:40:26 +0100 Subject: [PATCH 06/11] save search state between uses and restarts --- .../fdroid/views/apps/AppListActivity.java | 51 ++++++++++++++++--- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/views/apps/AppListActivity.java b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListActivity.java index 32594d952..d96da97f2 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/apps/AppListActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListActivity.java @@ -20,11 +20,14 @@ package org.fdroid.fdroid.views.apps; +import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.database.Cursor; import android.graphics.Color; import android.graphics.drawable.Drawable; import android.os.Bundle; +import android.text.TextUtils; import android.view.KeyEvent; import android.view.View; import android.view.inputmethod.EditorInfo; @@ -63,11 +66,15 @@ public class AppListActivity extends AppCompatActivity implements LoaderManager. public static final String EXTRA_SEARCH_TERMS = "org.fdroid.fdroid.views.apps.AppListActivity.EXTRA_SEARCH_TERMS"; + private static final String SEARCH_TERMS_KEY = "searchTerms"; + private static final String SORT_CLAUSE_KEY = "sortClauseSelected"; + private static SharedPreferences savedSearchSettings; + private RecyclerView appView; private AppListAdapter appAdapter; private String category; private String searchTerms; - private String sortClauseSelected = SortClause.LAST_UPDATED; + private String sortClauseSelected; private TextView emptyState; private EditText searchInput; private ImageView sortImage; @@ -87,7 +94,12 @@ public class AppListActivity extends AppCompatActivity implements LoaderManager. keyboardStateMonitor = new Utils.KeyboardStateMonitor(findViewById(R.id.app_list_root)); + savedSearchSettings = getSavedSearchSettings(this); + searchTerms = savedSearchSettings.getString(SEARCH_TERMS_KEY, null); + sortClauseSelected = savedSearchSettings.getString(SORT_CLAUSE_KEY, SortClause.LAST_UPDATED); + searchInput = (EditText) findViewById(R.id.search); + searchInput.setText(searchTerms); searchInput.addTextChangedListener(new CategoryTextWatcher(this, searchInput, this)); searchInput.setOnEditorActionListener(new TextView.OnEditorActionListener() { @Override @@ -110,24 +122,26 @@ public class AppListActivity extends AppCompatActivity implements LoaderManager. final Drawable lastUpdated = DrawableCompat.wrap(ContextCompat.getDrawable(this, R.drawable.ic_access_time)).mutate(); DrawableCompat.setTint(lastUpdated, FDroidApp.isAppThemeLight() ? Color.BLACK : Color.WHITE); - sortImage.setImageDrawable(lastUpdated); + final Drawable words = DrawableCompat.wrap(ContextCompat.getDrawable(AppListActivity.this, + R.drawable.ic_sort)).mutate(); + DrawableCompat.setTint(words, FDroidApp.isAppThemeLight() ? Color.BLACK : Color.WHITE); + sortImage.setImageDrawable(SortClause.WORDS.equals(sortClauseSelected) ? words : lastUpdated); sortImage.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { switch (sortClauseSelected) { case SortClause.WORDS: sortClauseSelected = SortClause.LAST_UPDATED; + DrawableCompat.setTint(lastUpdated, FDroidApp.isAppThemeLight() ? Color.BLACK : Color.WHITE); sortImage.setImageDrawable(lastUpdated); break; case SortClause.LAST_UPDATED: sortClauseSelected = SortClause.WORDS; - final Drawable alphabetical = DrawableCompat.wrap( - ContextCompat.getDrawable(AppListActivity.this, R.drawable.ic_sort)) - .mutate(); - DrawableCompat.setTint(alphabetical, FDroidApp.isAppThemeLight() ? Color.BLACK : Color.WHITE); - sortImage.setImageDrawable(alphabetical); + DrawableCompat.setTint(words, FDroidApp.isAppThemeLight() ? Color.BLACK : Color.WHITE); + sortImage.setImageDrawable(words); break; } + putSavedSearchSettings(getApplicationContext(), SORT_CLAUSE_KEY, sortClauseSelected); getSupportLoaderManager().restartLoader(0, null, AppListActivity.this); appView.scrollToPosition(0); } @@ -250,6 +264,11 @@ public class AppListActivity extends AppCompatActivity implements LoaderManager. this.category = category; this.searchTerms = searchTerms; getSupportLoaderManager().restartLoader(0, null, this); + if (TextUtils.isEmpty(searchTerms)) { + removeSavedSearchSettings(this, SEARCH_TERMS_KEY); + } else { + putSavedSearchSettings(this, SEARCH_TERMS_KEY, searchTerms); + } } private String getSortOrder() { @@ -328,4 +347,22 @@ public class AppListActivity extends AppCompatActivity implements LoaderManager. + " THEN 1 ELSE 0 END" + ", " + table + "." + Cols.LAST_UPDATED + " DESC"; } + + public static void putSavedSearchSettings(Context context, String key, String searchTerms) { + if (savedSearchSettings == null) { + savedSearchSettings = getSavedSearchSettings(context); + } + savedSearchSettings.edit().putString(key, searchTerms).apply(); + } + + public static void removeSavedSearchSettings(Context context, String key) { + if (savedSearchSettings == null) { + savedSearchSettings = getSavedSearchSettings(context); + } + savedSearchSettings.edit().remove(key).apply(); + } + + private static SharedPreferences getSavedSearchSettings(Context context) { + return context.getSharedPreferences("saved-search-settings", Context.MODE_PRIVATE); + } } From e9b03e5a2a0bd816baf6e06c9d0e3c7f1b5f96fc Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Mon, 8 Feb 2021 23:56:28 +0100 Subject: [PATCH 07/11] properly pad and scale the search sort button --- app/src/main/res/drawable/ic_access_time.xml | 22 ++++++++++++------- app/src/main/res/drawable/ic_sort.xml | 7 +++--- app/src/main/res/layout/activity_app_list.xml | 2 ++ 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/app/src/main/res/drawable/ic_access_time.xml b/app/src/main/res/drawable/ic_access_time.xml index 4f999321e..9cd21dd87 100644 --- a/app/src/main/res/drawable/ic_access_time.xml +++ b/app/src/main/res/drawable/ic_access_time.xml @@ -1,9 +1,15 @@ - - - + + + diff --git a/app/src/main/res/drawable/ic_sort.xml b/app/src/main/res/drawable/ic_sort.xml index 69d39e915..ce3583a0e 100644 --- a/app/src/main/res/drawable/ic_sort.xml +++ b/app/src/main/res/drawable/ic_sort.xml @@ -1,9 +1,10 @@ + android:viewportWidth="24"> diff --git a/app/src/main/res/layout/activity_app_list.xml b/app/src/main/res/layout/activity_app_list.xml index d30c8595a..3484e90d3 100644 --- a/app/src/main/res/layout/activity_app_list.xml +++ b/app/src/main/res/layout/activity_app_list.xml @@ -73,6 +73,8 @@ android:id="@+id/sort" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginStart="4dp" + android:layout_marginLeft="4dp" android:layout_marginEnd="4dp" android:layout_marginRight="4dp" android:contentDescription="@string/sort_search" From 2a4c9f0fcb2865f0f631e9ed45641a20bfffe76f Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Tue, 9 Feb 2021 10:06:40 +0100 Subject: [PATCH 08/11] sort "new" app entries based on lastUpdate time of Repo https://gitlab.com/fdroid/fdroidclient/-/issues/939#note_504169741 --- .../fdroid/fdroid/views/categories/AppCardController.java | 6 ++---- .../org/fdroid/fdroid/views/main/LatestViewBinder.java | 7 ++++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/views/categories/AppCardController.java b/app/src/main/java/org/fdroid/fdroid/views/categories/AppCardController.java index 2d7a09bb2..138427e6f 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/categories/AppCardController.java +++ b/app/src/main/java/org/fdroid/fdroid/views/categories/AppCardController.java @@ -1,20 +1,18 @@ package org.fdroid.fdroid.views.categories; -import androidx.appcompat.app.AppCompatActivity; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.widget.ImageView; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityOptionsCompat; import androidx.core.content.ContextCompat; import androidx.core.util.Pair; import androidx.core.view.ViewCompat; import androidx.recyclerview.widget.RecyclerView; - import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.App; @@ -33,7 +31,7 @@ public class AppCardController extends RecyclerView.ViewHolder /** * After this many days, don't consider showing the "New" tag next to an app. */ - private static final int DAYS_TO_CONSIDER_NEW = 14; + public static final int DAYS_TO_CONSIDER_NEW = 14; @NonNull private final ImageView icon; diff --git a/app/src/main/java/org/fdroid/fdroid/views/main/LatestViewBinder.java b/app/src/main/java/org/fdroid/fdroid/views/main/LatestViewBinder.java index a53315ce6..685136b84 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/main/LatestViewBinder.java +++ b/app/src/main/java/org/fdroid/fdroid/views/main/LatestViewBinder.java @@ -25,8 +25,10 @@ import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.data.Schema.AppMetadataTable; import org.fdroid.fdroid.data.Schema.AppMetadataTable.Cols; +import org.fdroid.fdroid.data.Schema.RepoTable; import org.fdroid.fdroid.panic.HidingManager; import org.fdroid.fdroid.views.apps.AppListActivity; +import org.fdroid.fdroid.views.categories.AppCardController; import java.util.Date; @@ -134,7 +136,10 @@ class LatestViewBinder implements LoaderManager.LoaderCallbacks { + " 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 + ")" + + " AND date((SELECT " + RepoTable.Cols.LAST_UPDATED + " FROM " + RepoTable.NAME + + " WHERE _id=" + table + "." + Cols.REPO_ID + + " ),'-" + AppCardController.DAYS_TO_CONSIDER_NEW + " days') " + + " < date(" + lastUpdated + ")" + " THEN 0 ELSE 1 END" + ", " + table + "." + Cols.WHATSNEW + " IS NULL ASC" + ", " + lastUpdated + " DESC" From be9b6515cdf843e67d7515b4033ab1729384f00e Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Tue, 9 Feb 2021 11:47:32 +0100 Subject: [PATCH 09/11] wipe "known categories" cache when database transients are reset closes #1626 closes #1632 --- app/src/main/java/org/fdroid/fdroid/data/AppProvider.java | 1 + app/src/main/java/org/fdroid/fdroid/data/CategoryProvider.java | 3 ++- app/src/main/java/org/fdroid/fdroid/data/DBHelper.java | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) 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 910ab70c2..389555883 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java @@ -983,6 +983,7 @@ public class AppProvider extends FDroidProvider { categoryValues.put(CatJoinTable.Cols.CATEGORY_ID, categoryId); db().insert(getCatJoinTableName(), null, categoryValues); } + getContext().getContentResolver().notifyChange(CategoryProvider.getContentUri(), null); } } diff --git a/app/src/main/java/org/fdroid/fdroid/data/CategoryProvider.java b/app/src/main/java/org/fdroid/fdroid/data/CategoryProvider.java index 8186fda0a..7da97e339 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/CategoryProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/CategoryProvider.java @@ -17,6 +17,7 @@ import java.util.Locale; import java.util.Map; public class CategoryProvider extends FDroidProvider { + public static final String TAG = "CategoryProvider"; public static final class Helper { private Helper() { @@ -26,7 +27,7 @@ public class CategoryProvider extends FDroidProvider { * During repo updates, each app needs to know the ID of each category it belongs to. * This results in lots of database lookups, usually at least one for each app, sometimes more. * To improve performance, this caches the association between categories and their database IDs. - * + *

* It can stay around for the entire F-Droid process, even across multiple repo updates, as we * don't actually remove data from the categories table. */ 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 ca30208d2..20f19860a 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java @@ -1318,6 +1318,7 @@ public class DBHelper extends SQLiteOpenHelper { Utils.debugLog(TAG, "Removing all index tables, they will be recreated next time F-Droid updates."); Preferences.get().resetLastUpdateCheck(); + CategoryProvider.Helper.clearCategoryIdCache(); db.beginTransaction(); try { From a0c809f0ad519a1ecb948791b49bb5238ef58b36 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Tue, 9 Feb 2021 12:19:58 +0100 Subject: [PATCH 10/11] onCreateLoader() is @NonNull, throw useful error instead --- .../views/main/CategoriesViewBinder.java | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/app/src/full/java/org/fdroid/fdroid/views/main/CategoriesViewBinder.java b/app/src/full/java/org/fdroid/fdroid/views/main/CategoriesViewBinder.java index cde674d31..4d636053c 100644 --- a/app/src/full/java/org/fdroid/fdroid/views/main/CategoriesViewBinder.java +++ b/app/src/full/java/org/fdroid/fdroid/views/main/CategoriesViewBinder.java @@ -3,28 +3,28 @@ package org.fdroid.fdroid.views.main; import android.content.Intent; import android.database.Cursor; import android.os.Bundle; -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.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; import android.view.View; import android.widget.FrameLayout; 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.LinearLayoutManager; +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; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.CategoryProvider; import org.fdroid.fdroid.data.Schema; +import org.fdroid.fdroid.panic.HidingManager; import org.fdroid.fdroid.views.apps.AppListActivity; import org.fdroid.fdroid.views.categories.CategoryAdapter; import org.fdroid.fdroid.views.categories.CategoryController; -import org.fdroid.fdroid.panic.HidingManager; import java.util.ArrayList; import java.util.Collections; @@ -37,6 +37,7 @@ import java.util.List; * view with relevant info about each. */ class CategoriesViewBinder implements LoaderManager.LoaderCallbacks { + public static final String TAG = "CategoriesViewBinder"; private static final int LOADER_ID = 429820532; @@ -92,10 +93,11 @@ class CategoriesViewBinder implements LoaderManager.LoaderCallbacks { activity.getSupportLoaderManager().restartLoader(LOADER_ID, null, this); } + @NonNull @Override public Loader onCreateLoader(int id, Bundle args) { if (id != LOADER_ID) { - return null; + throw new IllegalArgumentException("id != LOADER_ID"); } return new CursorLoader( @@ -110,7 +112,7 @@ class CategoriesViewBinder implements LoaderManager.LoaderCallbacks { /** * Reads all categories from the cursor and stores them in memory to provide to the {@link CategoryAdapter}. - * + *

* It does this so it is easier to deal with localized/unlocalized categories without having * to store the localized version in the database. It is not expected that the list of categories * will grow so large as to make this a performance concern. If it does in the future, the From 22cffbb0d851b554567c7d569703a27553056fcc Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Tue, 9 Feb 2021 12:15:35 +0100 Subject: [PATCH 11/11] 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. --- .../main/java/org/fdroid/fdroid/Utils.java | 2 +- .../org/fdroid/fdroid/data/AppProvider.java | 17 ++++++- .../views/categories/CategoryController.java | 51 +++++++++++++++---- .../fdroid/data/CategoryProviderTest.java | 18 ++++--- 4 files changed, 70 insertions(+), 18 deletions(-) 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); }