Merge branch 'search-overhaul' into 'master'
overhaul the search based on two recent merge requests See merge request fdroid/fdroidclient!972
This commit is contained in:
commit
0e38288705
@ -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<Cursor> {
|
||||
public static final String TAG = "CategoriesViewBinder";
|
||||
|
||||
private static final int LOADER_ID = 429820532;
|
||||
|
||||
@ -92,10 +93,11 @@ class CategoriesViewBinder implements LoaderManager.LoaderCallbacks<Cursor> {
|
||||
activity.getSupportLoaderManager().restartLoader(LOADER_ID, null, this);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Cursor> 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<Cursor> {
|
||||
|
||||
/**
|
||||
* Reads all categories from the cursor and stores them in memory to provide to the {@link CategoryAdapter}.
|
||||
*
|
||||
* <p>
|
||||
* 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
|
||||
|
@ -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 =
|
||||
|
@ -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;
|
||||
|
||||
@ -852,37 +861,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
|
||||
@ -912,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) {
|
||||
@ -1014,6 +998,7 @@ public class AppProvider extends FDroidProvider {
|
||||
categoryValues.put(CatJoinTable.Cols.CATEGORY_ID, categoryId);
|
||||
db().insert(getCatJoinTableName(), null, categoryValues);
|
||||
}
|
||||
getContext().getContentResolver().notifyChange(CategoryProvider.getContentUri(), null);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
*/
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
@ -32,7 +35,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 +45,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,25 +59,30 @@ import org.fdroid.fdroid.data.Schema;
|
||||
public class AppListActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor>,
|
||||
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
|
||||
= "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;
|
||||
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 WORDS = Cols.NAME;
|
||||
String LAST_UPDATED = Cols.LAST_UPDATED;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -88,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
|
||||
@ -111,20 +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) {
|
||||
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.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;
|
||||
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);
|
||||
}
|
||||
@ -218,10 +235,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()
|
||||
);
|
||||
}
|
||||
|
||||
@ -247,5 +264,105 @@ 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() {
|
||||
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";
|
||||
}
|
||||
|
||||
// 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 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]));
|
||||
}
|
||||
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"
|
||||
+ ", " + 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";
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
@ -43,7 +44,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;
|
||||
|
||||
@ -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,
|
||||
@ -148,7 +164,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,
|
||||
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -231,7 +262,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.
|
||||
|
@ -24,8 +24,11 @@ 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.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;
|
||||
|
||||
@ -94,6 +97,13 @@ class LatestViewBinder implements LoaderManager.LoaderCallbacks<Cursor> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 +112,38 @@ class LatestViewBinder implements LoaderManager.LoaderCallbacks<Cursor> {
|
||||
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((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"
|
||||
+ ", " + added + " ASC");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1,9 +1,15 @@
|
||||
<vector xmlns:tools="http://schemas.android.com/tools"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
tools:ignore="VectorRaster">
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z"/>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:width="36dp"
|
||||
android:height="36dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
tools:ignore="VectorRaster">
|
||||
<path
|
||||
android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"
|
||||
android:fillColor="#FFFFFF" />
|
||||
<path
|
||||
android:pathData="M12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z"
|
||||
android:fillColor="#FFFFFF" />
|
||||
</vector>
|
||||
|
11
app/src/main/res/drawable/ic_sort.xml
Normal file
11
app/src/main/res/drawable/ic_sort.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:autoMirrored="true"
|
||||
android:height="36dp"
|
||||
android:width="36dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M3,18h6v-2L3,16v2zM3,6v2h18L21,6L3,6zM3,13h12v-2L3,11v2z" />
|
||||
</vector>
|
@ -1,10 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M14.94,4.66h-4.72l2.36,-2.36zM10.25,19.37h4.66l-2.33,2.33zM6.1,6.27L1.6,17.73h1.84l0.92,-2.45h5.11l0.92,2.45h1.84L7.74,6.27L6.1,6.27zM4.97,13.64l1.94,-5.18 1.94,5.18L4.97,13.64zM15.73,16.14h6.12v1.59h-8.53v-1.29l5.92,-8.56h-5.88v-1.6h8.3v1.26l-5.93,8.6z" />
|
||||
</vector>
|
@ -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"
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user