diff --git a/app/src/main/java/org/fdroid/fdroid/data/App.java b/app/src/main/java/org/fdroid/fdroid/data/App.java index f75e4a844..978effb62 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/App.java +++ b/app/src/main/java/org/fdroid/fdroid/data/App.java @@ -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, 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; diff --git a/app/src/main/java/org/fdroid/fdroid/views/apps/AppListActivity.java b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListActivity.java index 708a324fa..b183e5b26 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/apps/AppListActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListActivity.java @@ -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, - 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> 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 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 loader, Cursor cursor) { + private void onAppsLoaded(List 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 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); diff --git a/app/src/main/java/org/fdroid/fdroid/views/apps/AppListAdapter.java b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListAdapter.java index 79e30e111..8188c0bd1 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/apps/AppListAdapter.java +++ b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListAdapter.java @@ -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 { - private Cursor cursor; + private final List 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 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 @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 } } - @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(); } } diff --git a/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemController.java b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemController.java index 55862c71b..86e3fbe5a 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemController.java +++ b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemController.java @@ -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 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); + }); } } diff --git a/app/src/main/java/org/fdroid/fdroid/views/apps/StandardAppListItemController.java b/app/src/main/java/org/fdroid/fdroid/views/apps/StandardAppListItemController.java index 29f3463b4..4d3644b6b 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/apps/StandardAppListItemController.java +++ b/app/src/main/java/org/fdroid/fdroid/views/apps/StandardAppListItemController.java @@ -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; diff --git a/app/src/main/res/layout/activity_app_list.xml b/app/src/main/res/layout/activity_app_list.xml index fe14d918b..be72ffa2f 100644 --- a/app/src/main/res/layout/activity_app_list.xml +++ b/app/src/main/res/layout/activity_app_list.xml @@ -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"/>