[app] App list search with new DB

This commit is contained in:
Torsten Grote
2022-03-31 14:12:18 -03:00
committed by Hans-Christoph Steiner
parent c4e92fba86
commit ad2700a0db
6 changed files with 107 additions and 165 deletions

View File

@@ -27,6 +27,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.commons.io.filefilter.RegexFileFilter;
import org.fdroid.database.AppListItem;
import org.fdroid.database.Repository;
import org.fdroid.database.UpdatableApp;
import org.fdroid.download.DownloadRequest;
@@ -514,6 +515,19 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
setInstalled(packageInfo);
}
public App(AppListItem item) {
repoId = item.getRepoId();
packageName = item.getPackageName();
name = item.getName() == null ? "" : item.getName();
summary = item.getSummary() == null ? "" : item.getSummary();
FileV2 iconFile = item.getIcon(getLocales());
iconFromApk = iconFile == null ? null : iconFile.getName();
installedVersionCode = item.getInstalledVersionCode() == null ? 0 : item.getInstalledVersionCode().intValue();
installedVersionName = item.getInstalledVersionName();
antiFeatures = item.getAntiFeatureKeys().toArray(new String[0]);
compatible = item.isCompatible();
}
public void setInstalled(@Nullable PackageInfo packageInfo) {
installedVersionCode = packageInfo == null ? 0 : packageInfo.versionCode;
installedVersionName = packageInfo == null ? null : packageInfo.versionName;

View File

@@ -23,9 +23,6 @@ package org.fdroid.fdroid.views.apps;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.KeyEvent;
@@ -39,12 +36,14 @@ import android.widget.TextView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
import org.fdroid.database.AppListItem;
import org.fdroid.database.AppListSortOrder;
import org.fdroid.database.FDroidDatabase;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.Schema.AppMetadataTable;
import org.fdroid.fdroid.data.DBHelper;
import org.fdroid.fdroid.data.Schema.AppMetadataTable.Cols;
import org.fdroid.fdroid.views.main.MainActivity;
@@ -52,18 +51,16 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.CursorLoader;
import androidx.loader.content.Loader;
import androidx.lifecycle.LiveData;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
/**
* Provides scrollable listing of apps for search and category views.
*/
public class AppListActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor>,
CategoryTextWatcher.SearchTermsChangedListener {
public class AppListActivity extends AppCompatActivity implements CategoryTextWatcher.SearchTermsChangedListener {
public static final String TAG = "AppListActivity";
@@ -85,7 +82,9 @@ public class AppListActivity extends AppCompatActivity implements LoaderManager.
private EditText searchInput;
private ImageView sortImage;
private View hiddenAppNotice;
private FDroidDatabase db;
private Utils.KeyboardStateMonitor keyboardStateMonitor;
private LiveData<List<AppListItem>> itemsLiveData;
private interface SortClause {
String WORDS = Cols.NAME;
@@ -101,6 +100,7 @@ public class AppListActivity extends AppCompatActivity implements LoaderManager.
setContentView(R.layout.activity_app_list);
db = DBHelper.getDb(this.getApplicationContext());
keyboardStateMonitor = new Utils.KeyboardStateMonitor(findViewById(R.id.app_list_root));
savedSearchSettings = getSavedSearchSettings(this);
@@ -128,28 +128,24 @@ public class AppListActivity extends AppCompatActivity implements LoaderManager.
});
sortImage = (ImageView) findViewById(R.id.sort);
final Drawable lastUpdated = DrawableCompat.wrap(ContextCompat.getDrawable(this,
R.drawable.ic_last_updated)).mutate();
final Drawable words = DrawableCompat.wrap(ContextCompat.getDrawable(AppListActivity.this,
R.drawable.ic_sort)).mutate();
sortImage.setImageDrawable(SortClause.WORDS.equals(sortClauseSelected) ? words : lastUpdated);
sortImage.setImageResource(
SortClause.WORDS.equals(sortClauseSelected) ? R.drawable.ic_sort : R.drawable.ic_last_updated
);
sortImage.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
switch (sortClauseSelected) {
case SortClause.WORDS:
sortClauseSelected = SortClause.LAST_UPDATED;
DrawableCompat.setTint(lastUpdated, FDroidApp.isAppThemeLight() ? Color.BLACK : Color.WHITE);
sortImage.setImageDrawable(lastUpdated);
sortImage.setImageResource(R.drawable.ic_last_updated);
break;
case SortClause.LAST_UPDATED:
sortClauseSelected = SortClause.WORDS;
DrawableCompat.setTint(words, FDroidApp.isAppThemeLight() ? Color.BLACK : Color.WHITE);
sortImage.setImageDrawable(words);
sortImage.setImageResource(R.drawable.ic_sort);
break;
}
putSavedSearchSettings(getApplicationContext(), SORT_CLAUSE_KEY, sortClauseSelected);
getSupportLoaderManager().restartLoader(0, null, AppListActivity.this);
loadItems();
appView.scrollToPosition(0);
}
});
@@ -197,6 +193,7 @@ public class AppListActivity extends AppCompatActivity implements LoaderManager.
appView.setAdapter(appAdapter);
parseIntentForSearchQuery();
loadItems();
}
@Override
@@ -219,8 +216,22 @@ public class AppListActivity extends AppCompatActivity implements LoaderManager.
// experience where the user scrolls through the apps in the category.
appView.requestFocus();
}
}
getSupportLoaderManager().initLoader(0, null, this);
private void loadItems() {
if (itemsLiveData != null) {
itemsLiveData.removeObserver(this::onAppsLoaded);
}
AppListSortOrder sortOrder =
SortClause.WORDS.equals(sortClauseSelected) ? AppListSortOrder.NAME : AppListSortOrder.LAST_UPDATED;
if (category == null) {
itemsLiveData = db.getAppDao().getAppListItems(getPackageManager(), searchTerms,
sortOrder);
} else {
itemsLiveData = db.getAppDao().getAppListItems(getPackageManager(), category,
searchTerms, sortOrder);
}
itemsLiveData.observe(this, this::onAppsLoaded);
}
private CharSequence getSearchText(@Nullable String category, @Nullable String searchTerms) {
@@ -240,25 +251,11 @@ public class AppListActivity extends AppCompatActivity implements LoaderManager.
hiddenAppNotice.setVisibility(show ? View.VISIBLE : View.GONE);
}
@NonNull
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new CursorLoader(
this,
AppProvider.getSearchUri(searchTerms, category),
AppMetadataTable.Cols.ALL,
null,
null,
getSortOrder()
);
}
@Override
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {
private void onAppsLoaded(List<AppListItem> items) {
setShowHiddenAppsNotice(false);
appAdapter.setHasHiddenAppsCallback(() -> setShowHiddenAppsNotice(true));
appAdapter.setAppCursor(cursor);
if (cursor.getCount() > 0) {
appAdapter.setItems(items);
if (items.size() > 0) {
emptyState.setVisibility(View.GONE);
appView.setVisibility(View.VISIBLE);
} else {
@@ -267,17 +264,12 @@ public class AppListActivity extends AppCompatActivity implements LoaderManager.
}
}
@Override
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
appAdapter.setAppCursor(null);
}
@Override
public void onSearchTermsChanged(@Nullable String category, @NonNull String searchTerms) {
this.category = category;
this.searchTerms = searchTerms;
appView.scrollToPosition(0);
getSupportLoaderManager().restartLoader(0, null, this);
loadItems();
if (TextUtils.isEmpty(searchTerms)) {
removeSavedSearchSettings(this, SEARCH_TERMS_KEY);
} else {
@@ -285,87 +277,6 @@ public class AppListActivity extends AppCompatActivity implements LoaderManager.
}
}
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"
+ ", CASE WHEN " + table + "." + Cols.ICON + " IS NULL"
+ " AND " + table + "." + Cols.ICON_URL + " IS NULL"
+ " THEN 1 ELSE 0 END"
+ ", " + 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"
+ ", CASE WHEN " + table + "." + Cols.ICON + " IS NULL"
+ " AND " + table + "." + Cols.ICON_URL + " IS NULL"
+ " THEN 1 ELSE 0 END"
+ ", " + 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);

View File

@@ -1,34 +1,37 @@
package org.fdroid.fdroid.views.apps;
import android.database.Cursor;
import android.view.View;
import android.view.ViewGroup;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.Schema;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.RecyclerView;
import org.fdroid.database.AppListItem;
import java.util.ArrayList;
import java.util.List;
class AppListAdapter extends RecyclerView.Adapter<StandardAppListItemController> {
private Cursor cursor;
private final List<AppListItem> items = new ArrayList<>();
private Runnable hasHiddenAppsCallback;
private final AppCompatActivity activity;
AppListAdapter(AppCompatActivity activity) {
this.activity = activity;
setHasStableIds(true);
}
public void setAppCursor(Cursor cursor) {
this.cursor = cursor;
void setItems(List<AppListItem> items) {
this.items.clear();
this.items.addAll(items);
notifyDataSetChanged();
}
public void setHasHiddenAppsCallback(Runnable callback) {
void setHasHiddenAppsCallback(Runnable callback) {
hasHiddenAppsCallback = callback;
}
@@ -41,8 +44,8 @@ class AppListAdapter extends RecyclerView.Adapter<StandardAppListItemController>
@Override
public void onBindViewHolder(@NonNull StandardAppListItemController holder, int position) {
cursor.moveToPosition(position);
final App app = new App(cursor);
AppListItem appItem = items.get(position);
final App app = new App(appItem);
holder.bindModel(app, null, null);
if (app.isDisabledByAntiFeatures(activity)) {
@@ -68,14 +71,8 @@ class AppListAdapter extends RecyclerView.Adapter<StandardAppListItemController>
}
}
@Override
public long getItemId(int position) {
cursor.moveToPosition(position);
return cursor.getLong(cursor.getColumnIndexOrThrow(Schema.AppMetadataTable.Cols.ROW_ID));
}
@Override
public int getItemCount() {
return cursor == null ? 0 : cursor.getCount();
return items.size();
}
}

View File

@@ -20,6 +20,18 @@ import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityOptionsCompat;
import androidx.core.content.ContextCompat;
import androidx.core.util.Pair;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.recyclerview.widget.RecyclerView;
import org.fdroid.database.AppVersion;
import org.fdroid.database.DbUpdateChecker;
import org.fdroid.database.FDroidDatabase;
import org.fdroid.fdroid.AppUpdateStatusManager;
import org.fdroid.fdroid.AppUpdateStatusManager.AppUpdateStatus;
import org.fdroid.fdroid.Preferences;
@@ -27,6 +39,7 @@ import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.DBHelper;
import org.fdroid.fdroid.installer.ApkCache;
import org.fdroid.fdroid.installer.InstallManagerService;
import org.fdroid.fdroid.installer.Installer;
@@ -36,16 +49,10 @@ import org.fdroid.fdroid.views.updates.UpdatesAdapter;
import java.io.File;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityOptionsCompat;
import androidx.core.content.ContextCompat;
import androidx.core.util.Pair;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.recyclerview.widget.RecyclerView;
import io.reactivex.rxjava3.disposables.Disposable;
/**
* Supports the following layouts:
@@ -110,6 +117,8 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder {
@Nullable
private AppUpdateStatus currentStatus;
@Nullable
private Disposable disposable;
@TargetApi(21)
public AppListItemController(final AppCompatActivity activity, View itemView) {
@@ -185,9 +194,8 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder {
return currentStatus;
}
public void bindModel(@NonNull App app, Apk apk, @Nullable AppUpdateStatus s) {
public void bindModel(@NonNull App app, @Nullable Apk apk, @Nullable AppUpdateStatus s) {
currentApp = app;
if (apk == null) throw new IllegalStateException(); // TODO remove at the end and make Apk @NonNull
currentApk = apk;
if (actionButton != null) actionButton.setEnabled(true);
@@ -490,8 +498,8 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder {
}
};
protected void onActionButtonPressed(App app, Apk apk) {
if (app == null || apk == null) {
protected void onActionButtonPressed(App app, @Nullable Apk apk) {
if (app == null) {
return;
}
@@ -537,7 +545,17 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder {
Installer installer = InstallerFactory.create(activity, currentStatus.apk);
installer.installPackage(Uri.parse(apkFilePath.toURI().toString()), canonicalUri);
} else {
InstallManagerService.queue(activity, app, apk);
FDroidDatabase db = DBHelper.getDb(activity);
DbUpdateChecker updateChecker = new DbUpdateChecker(db, activity.getPackageManager());
List<String> releaseChannels = Preferences.get().getBackendReleaseChannels();
if (disposable != null) disposable.dispose();
disposable = Utils.runOffUiThread(() -> {
AppVersion version = updateChecker.getSuggestedVersion(app.packageName,
app.preferredSigner, releaseChannels);
return version == null ? null : new Apk(version);
}, receivedApk -> {
if (receivedApk != null) InstallManagerService.queue(activity, app, receivedApk);
});
}
}

View File

@@ -28,21 +28,22 @@ public class StandardAppListItemController extends AppListItemController {
@Override
protected AppListItemState getCurrentViewState(
@NonNull App app, @Nullable AppUpdateStatusManager.AppUpdateStatus appStatus) {
AppUpdateStatusManager updateStatusManager = AppUpdateStatusManager.getInstance(itemView.getContext());
String versionName = updateStatusManager.getInstallableVersion(app.packageName);
return super.getCurrentViewState(app, appStatus)
.setStatusText(getStatusText(app))
.setShowInstallButton(shouldShowInstall(app));
.setStatusText(getStatusText(app, versionName))
.setShowInstallButton(shouldShowInstall(app, versionName));
}
@Nullable
private CharSequence getStatusText(@NonNull App app) {
private CharSequence getStatusText(@NonNull App app, @Nullable String versionName) {
if (!app.compatible) {
return activity.getString(R.string.app_incompatible);
} else if (app.antiFeatures != null && app.antiFeatures.length > 0) {
return activity.getString(R.string.antifeatures);
} else if (app.isInstalled(activity.getApplicationContext())) {
if (app.canAndWantToUpdate(activity)) {
return activity.getString(R.string.app_version_x_available, app.getAutoInstallVersionName());
} else if (app.installedVersionName != null) {
if (versionName != null) {
return activity.getString(R.string.app_version_x_available, versionName);
} else {
return activity.getString(R.string.app_version_x_installed, app.installedVersionName);
}
@@ -51,8 +52,8 @@ public class StandardAppListItemController extends AppListItemController {
return null;
}
private boolean shouldShowInstall(@NonNull App app) {
boolean installable = app.canAndWantToUpdate(activity) || !app.isInstalled(activity.getApplicationContext());
private boolean shouldShowInstall(@NonNull App app, @Nullable String versionName) {
boolean installable = versionName != null || app.installedVersionName == null;
boolean shouldAllow = app.compatible && (app.antiFeatures == null || app.antiFeatures.length == 0);
return installable && shouldAllow;

View File

@@ -80,7 +80,8 @@
app:layout_constraintTop_toTopOf="@id/search_card"
app:layout_constraintBottom_toBottomOf="@id/search_card"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/search_card"/>
app:layout_constraintStart_toEndOf="@+id/search_card"
app:tint="?attr/colorControlNormal"/>
<TextView
android:id="@+id/hiddenAppNotice"