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
	 Hans-Christoph Steiner
						Hans-Christoph Steiner