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.content.Intent;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.os.Bundle;
|
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.view.View;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.TextView;
|
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.Preferences;
|
||||||
import org.fdroid.fdroid.R;
|
import org.fdroid.fdroid.R;
|
||||||
import org.fdroid.fdroid.UpdateService;
|
import org.fdroid.fdroid.UpdateService;
|
||||||
import org.fdroid.fdroid.Utils;
|
import org.fdroid.fdroid.Utils;
|
||||||
import org.fdroid.fdroid.data.CategoryProvider;
|
import org.fdroid.fdroid.data.CategoryProvider;
|
||||||
import org.fdroid.fdroid.data.Schema;
|
import org.fdroid.fdroid.data.Schema;
|
||||||
|
import org.fdroid.fdroid.panic.HidingManager;
|
||||||
import org.fdroid.fdroid.views.apps.AppListActivity;
|
import org.fdroid.fdroid.views.apps.AppListActivity;
|
||||||
import org.fdroid.fdroid.views.categories.CategoryAdapter;
|
import org.fdroid.fdroid.views.categories.CategoryAdapter;
|
||||||
import org.fdroid.fdroid.views.categories.CategoryController;
|
import org.fdroid.fdroid.views.categories.CategoryController;
|
||||||
import org.fdroid.fdroid.panic.HidingManager;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@ -37,6 +37,7 @@ import java.util.List;
|
|||||||
* view with relevant info about each.
|
* view with relevant info about each.
|
||||||
*/
|
*/
|
||||||
class CategoriesViewBinder implements LoaderManager.LoaderCallbacks<Cursor> {
|
class CategoriesViewBinder implements LoaderManager.LoaderCallbacks<Cursor> {
|
||||||
|
public static final String TAG = "CategoriesViewBinder";
|
||||||
|
|
||||||
private static final int LOADER_ID = 429820532;
|
private static final int LOADER_ID = 429820532;
|
||||||
|
|
||||||
@ -92,10 +93,11 @@ class CategoriesViewBinder implements LoaderManager.LoaderCallbacks<Cursor> {
|
|||||||
activity.getSupportLoaderManager().restartLoader(LOADER_ID, null, this);
|
activity.getSupportLoaderManager().restartLoader(LOADER_ID, null, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||||
if (id != LOADER_ID) {
|
if (id != LOADER_ID) {
|
||||||
return null;
|
throw new IllegalArgumentException("id != LOADER_ID");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new CursorLoader(
|
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}.
|
* 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
|
* 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
|
* 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
|
* 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
|
// The date format used for storing dates (e.g. lastupdated, added) in the
|
||||||
// database.
|
// database.
|
||||||
private static final SimpleDateFormat DATE_FORMAT =
|
public static final SimpleDateFormat DATE_FORMAT =
|
||||||
new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
|
new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
|
||||||
|
|
||||||
private static final SimpleDateFormat TIME_FORMAT =
|
private static final SimpleDateFormat TIME_FORMAT =
|
||||||
|
@ -506,6 +506,9 @@ public class AppProvider extends FDroidProvider {
|
|||||||
return Uri.withAppendedPath(getContentUri(), PATH_CALC_SUGGESTED_APKS);
|
return Uri.withAppendedPath(getContentUri(), PATH_CALC_SUGGESTED_APKS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all {@link App} entries in the given {@code category}
|
||||||
|
*/
|
||||||
public static Uri getCategoryUri(String category) {
|
public static Uri getCategoryUri(String category) {
|
||||||
return getContentUri().buildUpon()
|
return getContentUri().buildUpon()
|
||||||
.appendPath(PATH_CATEGORY)
|
.appendPath(PATH_CATEGORY)
|
||||||
@ -519,6 +522,13 @@ public class AppProvider extends FDroidProvider {
|
|||||||
.build();
|
.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) {
|
public static Uri getTopFromCategoryUri(String category, int limit) {
|
||||||
return getContentUri().buildUpon()
|
return getContentUri().buildUpon()
|
||||||
.appendPath(PATH_TOP_FROM_CATEGORY)
|
.appendPath(PATH_TOP_FROM_CATEGORY)
|
||||||
@ -842,7 +852,6 @@ public class AppProvider extends FDroidProvider {
|
|||||||
case TOP_FROM_CATEGORY:
|
case TOP_FROM_CATEGORY:
|
||||||
selection = selection.add(queryCategory(pathSegments.get(2)));
|
selection = selection.add(queryCategory(pathSegments.get(2)));
|
||||||
limit = Integer.parseInt(pathSegments.get(1));
|
limit = Integer.parseInt(pathSegments.get(1));
|
||||||
sortOrder = getTableName() + "." + Cols.LAST_UPDATED + " DESC";
|
|
||||||
includeSwap = false;
|
includeSwap = false;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -852,37 +861,6 @@ public class AppProvider extends FDroidProvider {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case LATEST_TAB:
|
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
|
// 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.
|
// 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
|
// 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
|
* Helper method used by both the genuine {@link AppProvider} and the temporary version used
|
||||||
* by the repo updater ({@link TempAppProvider}).
|
* 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) {
|
protected Cursor runQuery(Uri uri, AppQuerySelection selection, String[] projection, boolean includeSwap, String sortOrder, int limit) {
|
||||||
if (!includeSwap) {
|
if (!includeSwap) {
|
||||||
@ -1014,6 +998,7 @@ public class AppProvider extends FDroidProvider {
|
|||||||
categoryValues.put(CatJoinTable.Cols.CATEGORY_ID, categoryId);
|
categoryValues.put(CatJoinTable.Cols.CATEGORY_ID, categoryId);
|
||||||
db().insert(getCatJoinTableName(), null, categoryValues);
|
db().insert(getCatJoinTableName(), null, categoryValues);
|
||||||
}
|
}
|
||||||
|
getContext().getContentResolver().notifyChange(CategoryProvider.getContentUri(), null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ import java.util.Locale;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public class CategoryProvider extends FDroidProvider {
|
public class CategoryProvider extends FDroidProvider {
|
||||||
|
public static final String TAG = "CategoryProvider";
|
||||||
|
|
||||||
public static final class Helper {
|
public static final class Helper {
|
||||||
private 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.
|
* 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.
|
* 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.
|
* 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
|
* 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.
|
* 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.");
|
Utils.debugLog(TAG, "Removing all index tables, they will be recreated next time F-Droid updates.");
|
||||||
|
|
||||||
Preferences.get().resetLastUpdateCheck();
|
Preferences.get().resetLastUpdateCheck();
|
||||||
|
CategoryProvider.Helper.clearCategoryIdCache();
|
||||||
|
|
||||||
db.beginTransaction();
|
db.beginTransaction();
|
||||||
try {
|
try {
|
||||||
|
@ -20,11 +20,14 @@
|
|||||||
|
|
||||||
package org.fdroid.fdroid.views.apps;
|
package org.fdroid.fdroid.views.apps;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.inputmethod.EditorInfo;
|
import android.view.inputmethod.EditorInfo;
|
||||||
@ -32,7 +35,6 @@ import android.view.inputmethod.InputMethodManager;
|
|||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
@ -43,14 +45,13 @@ import androidx.loader.content.CursorLoader;
|
|||||||
import androidx.loader.content.Loader;
|
import androidx.loader.content.Loader;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||||
|
|
||||||
import org.fdroid.fdroid.FDroidApp;
|
import org.fdroid.fdroid.FDroidApp;
|
||||||
import org.fdroid.fdroid.R;
|
import org.fdroid.fdroid.R;
|
||||||
import org.fdroid.fdroid.Utils;
|
import org.fdroid.fdroid.Utils;
|
||||||
import org.fdroid.fdroid.data.AppProvider;
|
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.
|
* 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>,
|
public class AppListActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor>,
|
||||||
CategoryTextWatcher.SearchTermsChangedListener {
|
CategoryTextWatcher.SearchTermsChangedListener {
|
||||||
|
|
||||||
|
public static final String TAG = "AppListActivity";
|
||||||
|
|
||||||
public static final String EXTRA_CATEGORY
|
public static final String EXTRA_CATEGORY
|
||||||
= "org.fdroid.fdroid.views.apps.AppListActivity.EXTRA_CATEGORY";
|
= "org.fdroid.fdroid.views.apps.AppListActivity.EXTRA_CATEGORY";
|
||||||
public static final String EXTRA_SEARCH_TERMS
|
public static final String EXTRA_SEARCH_TERMS
|
||||||
= "org.fdroid.fdroid.views.apps.AppListActivity.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 RecyclerView appView;
|
||||||
private AppListAdapter appAdapter;
|
private AppListAdapter appAdapter;
|
||||||
private String category;
|
private String category;
|
||||||
private String searchTerms;
|
private String searchTerms;
|
||||||
private String sortClauseSelected = SortClause.LAST_UPDATED;
|
private String sortClauseSelected;
|
||||||
private TextView emptyState;
|
private TextView emptyState;
|
||||||
private EditText searchInput;
|
private EditText searchInput;
|
||||||
private ImageView sortImage;
|
private ImageView sortImage;
|
||||||
private Utils.KeyboardStateMonitor keyboardStateMonitor;
|
private Utils.KeyboardStateMonitor keyboardStateMonitor;
|
||||||
|
|
||||||
private interface SortClause {
|
private interface SortClause {
|
||||||
String NAME = Schema.AppMetadataTable.NAME + "." + Schema.AppMetadataTable.Cols.NAME + " asc";
|
String WORDS = Cols.NAME;
|
||||||
String LAST_UPDATED = Schema.AppMetadataTable.NAME + "."
|
String LAST_UPDATED = Cols.LAST_UPDATED;
|
||||||
+ Schema.AppMetadataTable.Cols.LAST_UPDATED + " desc";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -88,7 +94,12 @@ public class AppListActivity extends AppCompatActivity implements LoaderManager.
|
|||||||
|
|
||||||
keyboardStateMonitor = new Utils.KeyboardStateMonitor(findViewById(R.id.app_list_root));
|
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 = (EditText) findViewById(R.id.search);
|
||||||
|
searchInput.setText(searchTerms);
|
||||||
searchInput.addTextChangedListener(new CategoryTextWatcher(this, searchInput, this));
|
searchInput.addTextChangedListener(new CategoryTextWatcher(this, searchInput, this));
|
||||||
searchInput.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
searchInput.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
||||||
@Override
|
@Override
|
||||||
@ -111,20 +122,26 @@ public class AppListActivity extends AppCompatActivity implements LoaderManager.
|
|||||||
final Drawable lastUpdated = DrawableCompat.wrap(ContextCompat.getDrawable(this,
|
final Drawable lastUpdated = DrawableCompat.wrap(ContextCompat.getDrawable(this,
|
||||||
R.drawable.ic_access_time)).mutate();
|
R.drawable.ic_access_time)).mutate();
|
||||||
DrawableCompat.setTint(lastUpdated, FDroidApp.isAppThemeLight() ? Color.BLACK : Color.WHITE);
|
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() {
|
sortImage.setOnClickListener(new View.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View view) {
|
public void onClick(View view) {
|
||||||
if (sortClauseSelected.equalsIgnoreCase(SortClause.LAST_UPDATED)) {
|
switch (sortClauseSelected) {
|
||||||
sortClauseSelected = SortClause.NAME;
|
case SortClause.WORDS:
|
||||||
final Drawable alphabetical = DrawableCompat.wrap(
|
sortClauseSelected = SortClause.LAST_UPDATED;
|
||||||
ContextCompat.getDrawable(AppListActivity.this, R.drawable.ic_sort_by_alpha)).mutate();
|
DrawableCompat.setTint(lastUpdated, FDroidApp.isAppThemeLight() ? Color.BLACK : Color.WHITE);
|
||||||
DrawableCompat.setTint(alphabetical, FDroidApp.isAppThemeLight() ? Color.BLACK : Color.WHITE);
|
sortImage.setImageDrawable(lastUpdated);
|
||||||
sortImage.setImageDrawable(alphabetical);
|
break;
|
||||||
} else {
|
case SortClause.LAST_UPDATED:
|
||||||
sortClauseSelected = SortClause.LAST_UPDATED;
|
sortClauseSelected = SortClause.WORDS;
|
||||||
sortImage.setImageDrawable(lastUpdated);
|
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);
|
getSupportLoaderManager().restartLoader(0, null, AppListActivity.this);
|
||||||
appView.scrollToPosition(0);
|
appView.scrollToPosition(0);
|
||||||
}
|
}
|
||||||
@ -218,10 +235,10 @@ public class AppListActivity extends AppCompatActivity implements LoaderManager.
|
|||||||
return new CursorLoader(
|
return new CursorLoader(
|
||||||
this,
|
this,
|
||||||
AppProvider.getSearchUri(searchTerms, category),
|
AppProvider.getSearchUri(searchTerms, category),
|
||||||
Schema.AppMetadataTable.Cols.ALL,
|
AppMetadataTable.Cols.ALL,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
sortClauseSelected
|
getSortOrder()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,5 +264,105 @@ public class AppListActivity extends AppCompatActivity implements LoaderManager.
|
|||||||
this.category = category;
|
this.category = category;
|
||||||
this.searchTerms = searchTerms;
|
this.searchTerms = searchTerms;
|
||||||
getSupportLoaderManager().restartLoader(0, null, this);
|
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;
|
package org.fdroid.fdroid.views.categories;
|
||||||
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.core.app.ActivityOptionsCompat;
|
import androidx.core.app.ActivityOptionsCompat;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.core.util.Pair;
|
import androidx.core.util.Pair;
|
||||||
import androidx.core.view.ViewCompat;
|
import androidx.core.view.ViewCompat;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import org.fdroid.fdroid.R;
|
import org.fdroid.fdroid.R;
|
||||||
import org.fdroid.fdroid.Utils;
|
import org.fdroid.fdroid.Utils;
|
||||||
import org.fdroid.fdroid.data.App;
|
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.
|
* 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
|
@NonNull
|
||||||
private final ImageView icon;
|
private final ImageView icon;
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package org.fdroid.fdroid.views.categories;
|
package org.fdroid.fdroid.views.categories;
|
||||||
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
@ -8,18 +7,19 @@ import android.database.Cursor;
|
|||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.os.Bundle;
|
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.view.View;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.TextView;
|
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.DisplayImageOptions;
|
||||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||||
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
|
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.Utils;
|
||||||
import org.fdroid.fdroid.data.AppProvider;
|
import org.fdroid.fdroid.data.AppProvider;
|
||||||
import org.fdroid.fdroid.data.Schema;
|
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.AppListActivity;
|
||||||
import org.fdroid.fdroid.views.apps.FeatureImage;
|
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 AppCompatActivity activity;
|
||||||
private final LoaderManager loaderManager;
|
private final LoaderManager loaderManager;
|
||||||
private final DisplayImageOptions displayImageOptions;
|
private final DisplayImageOptions displayImageOptions;
|
||||||
private static int categoryItemCount = 20;
|
private static final int NUM_OF_APPS_PER_CATEGORY_ON_OVERVIEW = 20;
|
||||||
|
|
||||||
private String currentCategory;
|
private String currentCategory;
|
||||||
|
|
||||||
@ -133,9 +134,24 @@ public class CategoryController extends RecyclerView.ViewHolder implements Loade
|
|||||||
return Color.HSVToColor(hsv);
|
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
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
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) {
|
if (id == currentCategory.hashCode() + 1) {
|
||||||
return new CursorLoader(
|
return new CursorLoader(
|
||||||
activity,
|
activity,
|
||||||
@ -148,7 +164,7 @@ public class CategoryController extends RecyclerView.ViewHolder implements Loade
|
|||||||
} else {
|
} else {
|
||||||
return new CursorLoader(
|
return new CursorLoader(
|
||||||
activity,
|
activity,
|
||||||
AppProvider.getTopFromCategoryUri(currentCategory, categoryItemCount),
|
AppProvider.getTopFromCategoryUri(currentCategory, NUM_OF_APPS_PER_CATEGORY_ON_OVERVIEW),
|
||||||
new String[]{
|
new String[]{
|
||||||
Schema.AppMetadataTable.Cols.NAME,
|
Schema.AppMetadataTable.Cols.NAME,
|
||||||
Schema.AppMetadataTable.Cols.Package.PACKAGE_NAME,
|
Schema.AppMetadataTable.Cols.Package.PACKAGE_NAME,
|
||||||
@ -159,7 +175,22 @@ public class CategoryController extends RecyclerView.ViewHolder implements Loade
|
|||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
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;
|
boolean isLtr = ViewCompat.getLayoutDirection(parent) == ViewCompat.LAYOUT_DIRECTION_LTR;
|
||||||
int itemPosition = parent.getChildLayoutPosition(view);
|
int itemPosition = parent.getChildLayoutPosition(view);
|
||||||
boolean first = itemPosition == 0;
|
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
|
// Leave this "paddingEnd" local variable here for clarity when converting from
|
||||||
// left/right to start/end for RTL friendly layout.
|
// 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.AppProvider;
|
||||||
import org.fdroid.fdroid.data.RepoProvider;
|
import org.fdroid.fdroid.data.RepoProvider;
|
||||||
import org.fdroid.fdroid.data.Schema.AppMetadataTable;
|
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.panic.HidingManager;
|
||||||
import org.fdroid.fdroid.views.apps.AppListActivity;
|
import org.fdroid.fdroid.views.apps.AppListActivity;
|
||||||
|
import org.fdroid.fdroid.views.categories.AppCardController;
|
||||||
|
|
||||||
import java.util.Date;
|
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()
|
* @see AppProvider#getLatestTabUri()
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
@ -102,14 +112,38 @@ class LatestViewBinder implements LoaderManager.LoaderCallbacks<Cursor> {
|
|||||||
if (id != LOADER_ID) {
|
if (id != LOADER_ID) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
final String table = AppMetadataTable.NAME;
|
||||||
|
final String added = table + "." + Cols.ADDED;
|
||||||
|
final String lastUpdated = table + "." + Cols.LAST_UPDATED;
|
||||||
return new CursorLoader(
|
return new CursorLoader(
|
||||||
activity,
|
activity,
|
||||||
AppProvider.getLatestTabUri(),
|
AppProvider.getLatestTabUri(),
|
||||||
AppMetadataTable.Cols.ALL,
|
AppMetadataTable.Cols.ALL,
|
||||||
null,
|
null,
|
||||||
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
|
@Override
|
||||||
|
@ -1,9 +1,15 @@
|
|||||||
<vector xmlns:tools="http://schemas.android.com/tools"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:height="24dp"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:tint="?attr/colorControlNormal"
|
android:width="36dp"
|
||||||
android:viewportHeight="24" android:viewportWidth="24"
|
android:height="36dp"
|
||||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"
|
android:tint="?attr/colorControlNormal"
|
||||||
tools:ignore="VectorRaster">
|
android:viewportWidth="24"
|
||||||
<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"/>
|
android:viewportHeight="24"
|
||||||
<path android:fillColor="#FFFFFF" android:pathData="M12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z"/>
|
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>
|
</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:id="@+id/sort"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:layout_marginLeft="4dp"
|
||||||
android:layout_marginEnd="4dp"
|
android:layout_marginEnd="4dp"
|
||||||
android:layout_marginRight="4dp"
|
android:layout_marginRight="4dp"
|
||||||
android:contentDescription="@string/sort_search"
|
android:contentDescription="@string/sort_search"
|
||||||
|
@ -6,6 +6,7 @@ import android.content.ContentValues;
|
|||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import org.fdroid.fdroid.TestUtils;
|
import org.fdroid.fdroid.TestUtils;
|
||||||
|
import org.fdroid.fdroid.Utils;
|
||||||
import org.fdroid.fdroid.data.Schema.AppMetadataTable.Cols;
|
import org.fdroid.fdroid.data.Schema.AppMetadataTable.Cols;
|
||||||
import org.fdroid.fdroid.mock.MockRepo;
|
import org.fdroid.fdroid.mock.MockRepo;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
@ -163,6 +164,11 @@ public class CategoryProviderTest extends FDroidProviderTest {
|
|||||||
AppProviderTest.assertContainsOnlyIds(apps, expectedPackages);
|
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
|
@Test
|
||||||
public void topAppsFromCategory() {
|
public void topAppsFromCategory() {
|
||||||
insertAppWithCategory("com.dog", "Dog", "Animal", new Date(2017, 2, 6));
|
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.banana", "Banana", "Vegetable", new Date(2015, 1, 1));
|
||||||
insertAppWithCategory("com.tomato", "Tomato", "Vegetable", new Date(2017, 4, 4));
|
insertAppWithCategory("com.tomato", "Tomato", "Vegetable", new Date(2017, 4, 4));
|
||||||
|
|
||||||
assertArrayEquals(getTopAppsFromCategory("Animal", 3), new String[]{"com.dog", "com.cat", "com.bird"});
|
assertArrayEquals(new String[]{"com.bird", "com.cat", "com.dog"}, getTopAppsFromCategory("Animal", 3));
|
||||||
assertArrayEquals(getTopAppsFromCategory("Animal", 2), new String[]{"com.dog", "com.cat"});
|
assertArrayEquals(new String[]{"com.bird", "com.cat"}, getTopAppsFromCategory("Animal", 2));
|
||||||
assertArrayEquals(getTopAppsFromCategory("Animal", 1), new String[]{"com.dog"});
|
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) {
|
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) {
|
private void insertAppWithCategory(String id, String name, String categories, Date lastUpdated, long repoId) {
|
||||||
ContentValues values = new ContentValues(2);
|
ContentValues values = new ContentValues(2);
|
||||||
values.put(Cols.ForWriting.Categories.CATEGORIES, categories);
|
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);
|
AppProviderTest.insertApp(contentResolver, context, id, name, values, repoId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user