From e6419b8f097b82e1bd2f86e5eaab47728b24448c Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 30 Mar 2022 15:30:07 -0300 Subject: [PATCH] [app] Make Updates tab use new DB --- .../SelectInstalledAppListItemController.java | 5 +- .../fdroid/fdroid/AppUpdateStatusManager.java | 89 ++++++- .../java/org/fdroid/fdroid/UpdateService.java | 156 +++++------ .../main/java/org/fdroid/fdroid/data/App.java | 44 +-- .../data/InstalledAppProviderService.java | 21 +- .../fdroid/views/AppDetailsActivity.java | 2 +- .../fdroid/views/apps/AppListAdapter.java | 2 +- .../views/apps/AppListItemController.java | 42 +-- .../installed/InstalledAppListAdapter.java | 2 +- .../fdroid/views/main/MainActivity.java | 72 +---- .../fdroid/views/updates/UpdatesAdapter.java | 252 +++++++----------- .../fdroid/views/updates/items/AppStatus.java | 2 +- .../items/AppStatusListItemController.java | 4 +- .../views/updates/items/KnownVulnApp.java | 13 +- .../items/KnownVulnAppListItemController.java | 74 ++--- .../views/updates/items/UpdateableApp.java | 7 +- .../UpdateableAppListItemController.java | 45 ++-- .../res/layout/known_vuln_app_list_item.xml | 3 +- 18 files changed, 368 insertions(+), 467 deletions(-) diff --git a/app/src/full/java/org/fdroid/fdroid/panic/SelectInstalledAppListItemController.java b/app/src/full/java/org/fdroid/fdroid/panic/SelectInstalledAppListItemController.java index 3b5642fe8..6498094ca 100644 --- a/app/src/full/java/org/fdroid/fdroid/panic/SelectInstalledAppListItemController.java +++ b/app/src/full/java/org/fdroid/fdroid/panic/SelectInstalledAppListItemController.java @@ -3,6 +3,7 @@ package org.fdroid.fdroid.panic; import android.view.View; import org.fdroid.fdroid.AppUpdateStatusManager; +import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.views.apps.AppListItemState; import org.fdroid.fdroid.views.installed.InstalledAppListItemController; @@ -33,7 +34,7 @@ public class SelectInstalledAppListItemController extends InstalledAppListItemCo } @Override - protected void onActionButtonPressed(App app) { - super.onActionButtonPressed(app); + protected void onActionButtonPressed(App app, Apk currentApk) { + super.onActionButtonPressed(app, currentApk); } } diff --git a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java index 839ff19b1..84b005bfa 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java +++ b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java @@ -9,8 +9,14 @@ import android.content.pm.PackageManager; import android.os.Parcel; import android.os.Parcelable; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import org.fdroid.database.UpdatableApp; +import org.fdroid.database.DbUpdateChecker; import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.App; +import org.fdroid.fdroid.data.DBHelper; import org.fdroid.fdroid.installer.ErrorDialogActivity; import org.fdroid.fdroid.installer.InstallManagerService; import org.fdroid.fdroid.net.DownloaderService; @@ -26,9 +32,10 @@ import java.util.Map; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.TaskStackBuilder; -import androidx.core.util.Pair; import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import io.reactivex.rxjava3.disposables.Disposable; + /** * Manages the state of APKs that are being installed or that have updates available. * This also ensures the state is saved across F-Droid restarts, and repopulates @@ -109,6 +116,7 @@ public final class AppUpdateStatusManager { } private static AppUpdateStatusManager instance; + private final MutableLiveData numUpdatableApps = new MutableLiveData<>(); public static class AppUpdateStatus implements Parcelable { public final App app; @@ -199,12 +207,19 @@ public final class AppUpdateStatusManager { private final Context context; private final LocalBroadcastManager localBroadcastManager; + private final DbUpdateChecker updateChecker; private final HashMap appMapping = new HashMap<>(); + @Nullable + private Disposable disposable; private boolean isBatchUpdating; private AppUpdateStatusManager(Context context) { this.context = context; localBroadcastManager = LocalBroadcastManager.getInstance(context.getApplicationContext()); + updateChecker = new DbUpdateChecker(DBHelper.getDb(context), context.getPackageManager()); + // let's check number of updatable apps at the beginning, so the badge can show the right number + // then we can also use the populated entries in other places to show updates + disposable = Utils.runOffUiThread(this::getUpdatableApps, this::addUpdatableAppsNoNotify); } public void removeAllByRepo(long repoId) { @@ -254,8 +269,39 @@ public final class AppUpdateStatusManager { return returnValues; } + /** + * Returns the version of the given package name that can be installed or is installing at the moment. + * If this returns null, no updates are available and no installs in progress. + */ + @Nullable + public String getInstallableVersion(String packageName) { + for (AppUpdateStatusManager.AppUpdateStatus status : getByPackageName(packageName)) { + AppUpdateStatusManager.Status s = status.status; + if (s != AppUpdateStatusManager.Status.DownloadInterrupted && + s != AppUpdateStatusManager.Status.Installed && + s != AppUpdateStatusManager.Status.InstallError) { + return status.apk.versionName; + } + } + return null; + } + + public LiveData getNumUpdatableApps() { + return numUpdatableApps; + } + + public void setNumUpdatableApps(int num) { + numUpdatableApps.postValue(num); + } + private void updateApkInternal(@NonNull AppUpdateStatus entry, @NonNull Status status, PendingIntent intent) { - Utils.debugLog(LOGTAG, "Update APK " + entry.apk.apkName + " state to " + status.name()); + if (status == Status.UpdateAvailable && entry.status.ordinal() > status.ordinal()) { + Utils.debugLog(LOGTAG, "Not updating APK " + entry.apk.apkName + " state to " + status.name()); + // If we have this entry in a more advanced state already, don't downgrade it + return; + } else { + Utils.debugLog(LOGTAG, "Update APK " + entry.apk.apkName + " state to " + status.name()); + } boolean isStatusUpdate = entry.status != status; entry.status = status; entry.intent = intent; @@ -264,6 +310,8 @@ public final class AppUpdateStatusManager { if (status == Status.Installed) { InstallManagerService.removePendingInstall(context, entry.getCanonicalUrl()); + // After an app got installed, update available updates + checkForUpdates(); } } @@ -323,12 +371,39 @@ public final class AppUpdateStatusManager { } } - public void addApks(List> apksToUpdate, Status status) { - startBatchUpdates(); - for (Pair pair : apksToUpdate) { - addApk(pair.first, pair.second, status, null); + public void checkForUpdates() { + if (disposable != null) disposable.dispose(); + disposable = Utils.runOffUiThread(this::getUpdatableApps, this::addUpdatableApps); + } + + private List getUpdatableApps() { + List releaseChannels = Preferences.get().getBackendReleaseChannels(); + return updateChecker.getUpdatableApps(releaseChannels); + } + + private void addUpdatableApps(List canUpdate) { + if (canUpdate.size() > 0) { + startBatchUpdates(); + for (UpdatableApp app : canUpdate) { + addApk(new App(app), new Apk(app.getUpdate()), Status.UpdateAvailable, null); + } + endBatchUpdates(Status.UpdateAvailable); + } + setNumUpdatableApps(canUpdate.size()); + } + + private void addUpdatableAppsNoNotify(List canUpdate) { + synchronized (appMapping) { + isBatchUpdating = true; + try { + for (UpdatableApp app : canUpdate) { + addApk(new App(app), new Apk(app.getUpdate()), Status.UpdateAvailable, null); + } + setNumUpdatableApps(canUpdate.size()); + } finally { + isBatchUpdating = false; + } } - endBatchUpdates(status); } /** diff --git a/app/src/main/java/org/fdroid/fdroid/UpdateService.java b/app/src/main/java/org/fdroid/fdroid/UpdateService.java index a138b6739..59ed5a307 100644 --- a/app/src/main/java/org/fdroid/fdroid/UpdateService.java +++ b/app/src/main/java/org/fdroid/fdroid/UpdateService.java @@ -37,37 +37,38 @@ import android.text.TextUtils; import android.util.Log; import android.widget.Toast; -import org.fdroid.CompatibilityChecker; -import org.fdroid.CompatibilityCheckerImpl; -import org.fdroid.database.Repository; -import org.fdroid.download.Mirror; -import org.fdroid.fdroid.data.Apk; -import org.fdroid.fdroid.data.ApkProvider; -import org.fdroid.fdroid.data.App; -import org.fdroid.fdroid.data.AppProvider; -import org.fdroid.fdroid.data.DBHelper; -import org.fdroid.fdroid.data.Schema; -import org.fdroid.fdroid.installer.InstallManagerService; -import org.fdroid.fdroid.net.BluetoothDownloader; -import org.fdroid.fdroid.net.ConnectivityMonitorService; -import org.fdroid.fdroid.net.DownloaderFactory; -import org.fdroid.index.v1.IndexUpdateListener; -import org.fdroid.index.v1.IndexUpdateResult; -import org.fdroid.index.v1.IndexUpdaterKt; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.JobIntentService; import androidx.core.app.NotificationCompat; import androidx.core.content.ContextCompat; -import androidx.core.util.Pair; import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import org.fdroid.CompatibilityChecker; +import org.fdroid.CompatibilityCheckerImpl; +import org.fdroid.database.FDroidDatabase; +import org.fdroid.database.Repository; +import org.fdroid.database.UpdatableApp; +import org.fdroid.database.DbUpdateChecker; +import org.fdroid.download.Mirror; +import org.fdroid.fdroid.data.Apk; +import org.fdroid.fdroid.data.App; +import org.fdroid.fdroid.data.DBHelper; +import org.fdroid.fdroid.installer.InstallManagerService; +import org.fdroid.fdroid.net.BluetoothDownloader; +import org.fdroid.fdroid.net.ConnectivityMonitorService; +import org.fdroid.fdroid.net.DownloaderFactory; +import org.fdroid.index.IndexUpdateResult; +import org.fdroid.index.RepoUpdater; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Completable; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.schedulers.Schedulers; public class UpdateService extends JobIntentService { @@ -105,6 +106,7 @@ public class UpdateService extends JobIntentService { private static UpdateService updateService; + private FDroidDatabase db; private NotificationManager notificationManager; private NotificationCompat.Builder notificationBuilder; private AppUpdateStatusManager appUpdateStatusManager; @@ -290,6 +292,7 @@ public class UpdateService extends JobIntentService { public void onCreate() { super.onCreate(); updateService = this; + db = DBHelper.getDb(getApplicationContext()); notificationManager = ContextCompat.getSystemService(this, NotificationManager.class); @@ -477,43 +480,33 @@ public class UpdateService extends JobIntentService { unchangedRepos++; continue; } + // TODO reject update if repo.getLastUpdated() is too recent + sendStatus(this, STATUS_INFO, getString(R.string.status_connecting_to_repo, repo.getAddress())); - try { - final String canonicalUri = IndexUpdaterKt.getCanonicalUri(repo).toString(); - final IndexUpdateListener listener = new UpdateServiceListener(this, canonicalUri); - final CompatibilityChecker compatChecker = - new CompatibilityCheckerImpl(getPackageManager(), Preferences.get().forceTouchApps()); - // TODO try new v2 index first - final org.fdroid.index.v1.IndexV1Updater updater = new org.fdroid.index.v1.IndexV1Updater( - getApplicationContext(), DownloaderFactory.INSTANCE, compatChecker); - final long currentRepoId = repo.getRepoId(); - final IndexUpdateResult result; - if (repo.getCertificate() == null) { - // This is a new repo without a certificate - result = updater.updateNewRepo(currentRepoId, fingerprint, listener); - } else { - result = updater.update(currentRepoId, repo.getCertificate(), listener); - } - if (result == IndexUpdateResult.UNCHANGED) { - unchangedRepos++; - } else if (result == IndexUpdateResult.PROCESSED) { - updatedRepos++; - changes = true; - } - } catch (Exception e) { + final CompatibilityChecker compatChecker = + new CompatibilityCheckerImpl(getPackageManager(), Preferences.get().forceTouchApps()); + final RepoUpdater repoUpdater = new RepoUpdater(getApplicationContext().getCacheDir(), db, + DownloaderFactory.INSTANCE, compatChecker, new UpdateServiceListener(UpdateService.this)); + final IndexUpdateResult result = repoUpdater.update(repo, fingerprint); + if (result instanceof IndexUpdateResult.Unchanged) { + unchangedRepos++; + } else if (result instanceof IndexUpdateResult.Processed) { + updatedRepos++; + changes = true; + } else if (result instanceof IndexUpdateResult.Error) { errorRepos++; + Exception e = ((IndexUpdateResult.Error) result).getE(); Throwable cause = e.getCause(); if (cause == null) { repoErrors.add(e.getLocalizedMessage()); } else { repoErrors.add(e.getLocalizedMessage() + " ⇨ " + cause.getLocalizedMessage()); } - Log.e(TAG, "Error updating repository " + repo.getAddress()); - e.printStackTrace(); + Log.e(TAG, "Error updating repository " + repo.getAddress(), e); } - // now that downloading the index is done, start downloading updates + // TODO why are we checking for updates several times (in loop and below) if (changes && fdroidPrefs.isAutoDownloadEnabled() && fdroidPrefs.isBackgroundDownloadAllowed()) { autoDownloadUpdates(this); } @@ -521,12 +514,8 @@ public class UpdateService extends JobIntentService { if (!changes) { Utils.debugLog(TAG, "Not checking app details or compatibility, because repos were up to date."); - } else { - notifyContentProviders(); - - if (fdroidPrefs.isUpdateNotificationEnabled() && !fdroidPrefs.isAutoDownloadEnabled()) { - performUpdateNotification(); - } + } else if (fdroidPrefs.isUpdateNotificationEnabled() && !fdroidPrefs.isAutoDownloadEnabled()) { + appUpdateStatusManager.checkForUpdates(); } fdroidPrefs.setLastUpdateCheck(System.currentTimeMillis()); @@ -553,49 +542,34 @@ public class UpdateService extends JobIntentService { Log.i(TAG, "Updating repo(s) complete, took " + time / 1000 + " seconds to complete."); } - private void notifyContentProviders() { - getContentResolver().notifyChange(AppProvider.getContentUri(), null); - getContentResolver().notifyChange(ApkProvider.getContentUri(), null); - } - - private void performUpdateNotification() { - List canUpdate = AppProvider.Helper.findCanUpdate(this, Schema.AppMetadataTable.Cols.ALL); - if (canUpdate.size() > 0) { - showAppUpdatesNotification(canUpdate); - } - } - /** * Queues all apps needing update. If this app itself (e.g. F-Droid) needs * to be updated, it is queued last. */ - public static void autoDownloadUpdates(Context context) { - // TODO adapt to new DB - List canUpdate = AppProvider.Helper.findCanUpdate(context, Schema.AppMetadataTable.Cols.ALL); - String packageName = context.getPackageName(); - App updateLastApp = null; - Apk updateLastApk = null; - for (App app : canUpdate) { - if (TextUtils.equals(packageName, app.packageName)) { - updateLastApp = app; - updateLastApk = ApkProvider.Helper.findSuggestedApk(context, app); - continue; - } - Apk apk = ApkProvider.Helper.findSuggestedApk(context, app); - InstallManagerService.queue(context, app, apk); - } - if (updateLastApp != null && updateLastApk != null) { - InstallManagerService.queue(context, updateLastApp, updateLastApk); - } + public static Disposable autoDownloadUpdates(Context context) { + DbUpdateChecker updateChecker = new DbUpdateChecker(DBHelper.getDb(context), context.getPackageManager()); + List releaseChannels = Preferences.get().getBackendReleaseChannels(); + return Single.fromCallable(() -> updateChecker.getUpdatableApps(releaseChannels)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(updatableApps -> downloadUpdates(context, updatableApps)); } - private void showAppUpdatesNotification(List canUpdate) { - if (canUpdate.size() > 0) { - List> apksToUpdate = new ArrayList<>(canUpdate.size()); - for (App app : canUpdate) { - apksToUpdate.add(new Pair<>(app, ApkProvider.Helper.findSuggestedApk(this, app))); + private static void downloadUpdates(Context context, List apps) { + String ourPackageName = context.getPackageName(); + App updateLastApp = null; + Apk updateLastApk = null; + for (UpdatableApp app : apps) { + // update our own APK at the end + if (TextUtils.equals(ourPackageName, app.getUpdate().getPackageName())) { + updateLastApp = new App(app); + updateLastApk = new Apk(app.getUpdate()); + continue; } - appUpdateStatusManager.addApks(apksToUpdate, AppUpdateStatusManager.Status.UpdateAvailable); + InstallManagerService.queue(context, new App(app), new Apk(app.getUpdate())); + } + if (updateLastApp != null) { + InstallManagerService.queue(context, updateLastApp, updateLastApk); } } 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 ee6014649..f75e4a844 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/App.java +++ b/app/src/main/java/org/fdroid/fdroid/data/App.java @@ -28,6 +28,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.apache.commons.io.filefilter.RegexFileFilter; import org.fdroid.database.Repository; +import org.fdroid.database.UpdatableApp; import org.fdroid.download.DownloadRequest; import org.fdroid.download.Mirror; import org.fdroid.fdroid.FDroidApp; @@ -433,6 +434,19 @@ public class App extends ValueObject implements Comparable, Parcelable { } } + public App(final UpdatableApp app) { + id = 0; + repoId = app.getUpdate().getRepoId(); + packageName = app.getPackageName(); + name = app.getName() == null ? "" : app.getName(); + summary = app.getSummary() == null ? "" : app.getSummary(); + installedVersionCode = (int) app.getInstalledVersionCode(); + autoInstallVersionCode = (int) app.getUpdate().getManifest().getVersionCode(); + FileV2 icon = app.getIcon(getLocales()); + iconUrl = icon == null ? null : icon.getName(); + iconFromApk = icon == null ? null : icon.getName(); + } + public App(final org.fdroid.database.App app, @Nullable PackageInfo packageInfo) { id = 0; repoId = app.getRepoId(); @@ -1116,36 +1130,6 @@ public class App extends ValueObject implements Comparable, Parcelable { apk.sig = Utils.getsig(rawCertBytes); } - /** - * Attempts to find the installed {@link Apk} from the database. If not found, will lookup the - * {@link InstalledAppProvider} to find the details of the installed app and use that to - * instantiate an {@link Apk} to be returned. - *

- * Cases where an {@link Apk} will not be found in the database and for which we fall back to - * the {@link InstalledAppProvider} include: - *

  • System apps which are provided by a repository, but for which the version code bundled - * with the system is not included in the repository.
  • - *
  • Regular apps from a repository, where the installed version is old enough that it is no - * longer available in the repository.
  • - */ - @Nullable - public Apk getInstalledApk(Context context) { - try { - PackageInfo pi = context.getPackageManager().getPackageInfo(this.packageName, 0); - Apk apk = ApkProvider.Helper.findApkFromAnyRepo(context, pi.packageName, pi.versionCode); - if (apk == null) { - InstalledApp installedApp = InstalledAppProvider.Helper.findByPackageName(context, pi.packageName); - if (installedApp == null) { - throw new IllegalStateException("No installed app found when trying to uninstall"); - } - apk = new Apk(installedApp); - } - return apk; - } catch (PackageManager.NameNotFoundException e) { - return null; - } - } - /** * Attempts to find the installed {@link Apk} in the given list of APKs. If not found, will lookup the * the details of the installed app and use that to instantiate an {@link Apk} to be returned. diff --git a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java index 8eb9dbb68..c61f8cedb 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java +++ b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java @@ -15,7 +15,6 @@ import android.os.RemoteException; import android.util.Log; import org.acra.ACRA; -import org.fdroid.fdroid.AppUpdateStatusManager; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.Schema.InstalledAppTable; import org.fdroid.fdroid.installer.PrivilegedInstaller; @@ -49,9 +48,9 @@ import io.reactivex.rxjava3.subjects.PublishSubject; * {@link #deleteAppFromDb(Context, String)} are both static methods to enable easy testing * of this stuff. *

    - * This also updates the {@link AppUpdateStatusManager.Status status} of any + * This also updates the {@link org.fdroid.fdroid.AppUpdateStatusManager.Status status} of any * package installs that are still in progress. Most importantly, this - * provides the final {@link AppUpdateStatusManager.Status#Installed status update} + * provides the final {@link org.fdroid.fdroid.AppUpdateStatusManager.Status#Installed status update} * to mark the end of the installation process. It also errors out installation * processes where some outside factor uninstalled the package while the F-Droid * process was underway, e.g. uninstalling via {@code adb}, updates via Google @@ -283,15 +282,16 @@ public class InstalledAppProviderService extends JobIntentService { protected void onHandleWork(@NonNull Intent intent) { Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST); - AppUpdateStatusManager ausm = AppUpdateStatusManager.getInstance(this); + //AppUpdateStatusManager ausm = AppUpdateStatusManager.getInstance(this); String packageName = intent.getData().getSchemeSpecificPart(); final String action = intent.getAction(); if (ACTION_INSERT.equals(action)) { PackageInfo packageInfo = getPackageInfo(intent, packageName); if (packageInfo != null) { - for (AppUpdateStatusManager.AppUpdateStatus status : ausm.getByPackageName(packageName)) { - ausm.updateApk(status.getCanonicalUrl(), AppUpdateStatusManager.Status.Installed, null); - } + //for (AppUpdateStatusManager.AppUpdateStatus status : ausm.getByPackageName(packageName)) { + // these cause duplicate events, do we really need this? + // ausm.updateApk(status.getCanonicalUrl(), AppUpdateStatusManager.Status.Installed, null); + //} File apk = getPathToInstalledApk(packageInfo); if (apk == null) { return; @@ -310,9 +310,10 @@ public class InstalledAppProviderService extends JobIntentService { } } else if (ACTION_DELETE.equals(action)) { deleteAppFromDb(this, packageName); - for (AppUpdateStatusManager.AppUpdateStatus status : ausm.getByPackageName(packageName)) { - ausm.updateApk(status.getCanonicalUrl(), AppUpdateStatusManager.Status.InstallError, null); - } + //for (AppUpdateStatusManager.AppUpdateStatus status : ausm.getByPackageName(packageName)) { + // these cause duplicate events, do we really need this? + // ausm.updateApk(status.getCanonicalUrl(), AppUpdateStatusManager.Status.InstallError, null); + //} } packageChangeNotifier.onNext(packageName); } diff --git a/app/src/main/java/org/fdroid/fdroid/views/AppDetailsActivity.java b/app/src/main/java/org/fdroid/fdroid/views/AppDetailsActivity.java index 22d84ca47..3dfbe459b 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/AppDetailsActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/AppDetailsActivity.java @@ -540,7 +540,7 @@ public class AppDetailsActivity extends AppCompatActivity && !TextUtils.equals(status.getCanonicalUrl(), currentStatus.getCanonicalUrl())) { Utils.debugLog(TAG, "Ignoring app status change because it belongs to " + status.getCanonicalUrl() + " not " + currentStatus.getCanonicalUrl()); - } else if (status != null && !TextUtils.equals(status.apk.packageName, app.packageName)) { + } else if (status != null && app != null && !TextUtils.equals(status.apk.packageName, app.packageName)) { Utils.debugLog(TAG, "Ignoring app status change because it belongs to " + status.apk.packageName + " not " + app.packageName); } else { 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 077f736d0..79e30e111 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 @@ -43,7 +43,7 @@ class AppListAdapter extends RecyclerView.Adapter public void onBindViewHolder(@NonNull StandardAppListItemController holder, int position) { cursor.moveToPosition(position); final App app = new App(cursor); - holder.bindModel(app); + holder.bindModel(app, null, null); if (app.isDisabledByAntiFeatures(activity)) { holder.itemView.setVisibility(View.GONE); 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 88d978db2..55862c71b 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 @@ -26,7 +26,6 @@ import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.Apk; -import org.fdroid.fdroid.data.ApkProvider; import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.installer.ApkCache; import org.fdroid.fdroid.installer.InstallManagerService; @@ -106,6 +105,8 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder { @Nullable private App currentApp; + @Nullable + private Apk currentApk; @Nullable private AppUpdateStatus currentStatus; @@ -123,7 +124,7 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder { installButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - onActionButtonPressed(currentApp); + onActionButtonPressed(currentApp, currentApk); } }); @@ -163,7 +164,7 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder { @Override public void onClick(View v) { actionButton.setEnabled(false); - onActionButtonPressed(currentApp); + onActionButtonPressed(currentApp, currentApk); } }); } @@ -184,23 +185,26 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder { return currentStatus; } - public void bindModel(@NonNull App app) { + public void bindModel(@NonNull App app, 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); Utils.setIconFromRepoOrPM(app, icon, activity); - // Figures out the current install/update/download/etc status for the app we are viewing. - // Then, asks the view to update itself to reflect this status. - Iterator statuses = - AppUpdateStatusManager.getInstance(activity).getByPackageName(app.packageName).iterator(); - if (statuses.hasNext()) { - AppUpdateStatus status = statuses.next(); - updateAppStatus(app, status); - } else { - updateAppStatus(app, null); + AppUpdateStatus status = s; + if (status == null) { + // Figures out the current install/update/download/etc status for the app we are viewing. + // Then, asks the view to update itself to reflect this status. + Iterator statuses = + AppUpdateStatusManager.getInstance(activity).getByPackageName(app.packageName).iterator(); + if (statuses.hasNext()) { + status = statuses.next(); + } } + updateAppStatus(app, status); final LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(activity.getApplicationContext()); @@ -265,6 +269,7 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder { if (actionButton != null) { if (viewState.shouldShowActionButton()) { actionButton.setVisibility(View.VISIBLE); + actionButton.setEnabled(true); actionButton.setText(viewState.getActionButtonText()); } else { actionButton.setVisibility(View.GONE); @@ -274,6 +279,7 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder { if (secondaryButton != null) { if (viewState.shouldShowSecondaryButton()) { secondaryButton.setVisibility(View.VISIBLE); + secondaryButton.setEnabled(true); secondaryButton.setText(viewState.getSecondaryButtonText()); } else { secondaryButton.setVisibility(View.GONE); @@ -399,6 +405,7 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder { .setStatusText(activity.getString(R.string.notification_content_single_installed)); if (activity.getPackageManager().getLaunchIntentForPackage(app.packageName) != null) { + Utils.debugLog(TAG, "Not showing 'Open' button for " + app.packageName + " because no intent."); state.showActionButton(activity.getString(R.string.menu_launch)); } @@ -478,13 +485,13 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder { if (currentApp == null) { return; } - + if (secondaryButton != null) secondaryButton.setEnabled(false); onSecondaryButtonPressed(currentApp); } }; - protected void onActionButtonPressed(App app) { - if (app == null) { + protected void onActionButtonPressed(App app, Apk apk) { + if (app == null || apk == null) { return; } @@ -530,8 +537,7 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder { Installer installer = InstallerFactory.create(activity, currentStatus.apk); installer.installPackage(Uri.parse(apkFilePath.toURI().toString()), canonicalUri); } else { - final Apk suggestedApk = ApkProvider.Helper.findSuggestedApk(activity, app); - InstallManagerService.queue(activity, app, suggestedApk); + InstallManagerService.queue(activity, app, apk); } } diff --git a/app/src/main/java/org/fdroid/fdroid/views/installed/InstalledAppListAdapter.java b/app/src/main/java/org/fdroid/fdroid/views/installed/InstalledAppListAdapter.java index 566af81d2..b009646a2 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/installed/InstalledAppListAdapter.java +++ b/app/src/main/java/org/fdroid/fdroid/views/installed/InstalledAppListAdapter.java @@ -50,7 +50,7 @@ public class InstalledAppListAdapter extends RecyclerView.Adapter @@ -63,26 +67,23 @@ import androidx.recyclerview.widget.RecyclerView; * repopulate it from the original source lists of data. When this is done, the adapter will notify * the recycler view that its data has changed. Sometimes it will also ask the recycler view to * scroll to the newly added item (if attached to the recycler view). - *

    - * TODO: If a user downloads an old version of an app (resulting in a new update being available - * instantly), then we need to refresh the list of apps to update. */ -public class UpdatesAdapter extends RecyclerView.Adapter - implements LoaderManager.LoaderCallbacks { - - private static final int LOADER_CAN_UPDATE = 289753982; - private static final int LOADER_KNOWN_VULN = 520389740; +public class UpdatesAdapter extends RecyclerView.Adapter { private final AdapterDelegatesManager> delegatesManager = new AdapterDelegatesManager<>(); - private final List items = new ArrayList<>(); private final AppCompatActivity activity; + private final DbUpdateChecker updateChecker; + private final List items = new ArrayList<>(); private final List appsToShowStatus = new ArrayList<>(); private final List updateableApps = new ArrayList<>(); private final List knownVulnApps = new ArrayList<>(); + // This is lost on configuration changes e.g. rotating the screen. private boolean showAllUpdateableApps = false; + @Nullable + private Disposable disposable; public UpdatesAdapter(AppCompatActivity activity) { this.activity = activity; @@ -90,9 +91,29 @@ public class UpdatesAdapter extends RecyclerView.Adapter releaseChannels = Preferences.get().getBackendReleaseChannels(); + if (disposable != null) disposable.dispose(); + disposable = Single.fromCallable(() -> updateChecker.getUpdatableApps(releaseChannels)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::onCanUpdateLoadFinished); + } + + public boolean canViewAllUpdateableApps() { + return showAllUpdateableApps; + } + + public void toggleAllUpdateableApps() { + showAllUpdateableApps = !showAllUpdateableApps; + refreshItems(); } /** @@ -110,71 +131,72 @@ public class UpdatesAdapter extends RecyclerView.Adapter apps) { + updateableApps.clear(); + knownVulnApps.clear(); + + for (UpdatableApp updatableApp: apps) { + App app = new App(updatableApp); + Apk apk = new Apk(updatableApp.getUpdate()); + if (updatableApp.getHasKnownVulnerability()) { + app.installedApk = apk; + knownVulnApps.add(new KnownVulnApp(activity, app, apk)); + } else { + updateableApps.add(new UpdateableApp(activity, app, apk)); + } + } + refreshItems(); + } + + @SuppressLint("NotifyDataSetChanged") + public void refreshItems() { + populateAppStatuses(); + populateItems(); + notifyDataSetChanged(); + } + /** * Adds items from the {@link AppUpdateStatusManager} to {@link UpdatesAdapter#appsToShowStatus}. - * Note that this will then subsequently rebuild the underlying adapter data structure by - * invoking {@link UpdatesAdapter#populateItems}. However as per the populateItems method, it - * does not know how best to notify the recycler view of any changes. That is up to the caller - * of this method. */ private void populateAppStatuses() { + appsToShowStatus.clear(); for (AppUpdateStatusManager.AppUpdateStatus status : AppUpdateStatusManager.getInstance(activity).getAll()) { if (shouldShowStatus(status)) { appsToShowStatus.add(new AppStatus(activity, status)); } } - - Collections.sort(appsToShowStatus, new Comparator() { - @Override - public int compare(AppStatus o1, AppStatus o2) { - return o1.status.app.name.compareTo(o2.status.app.name); - } - }); - - populateItems(); - } - - public boolean canViewAllUpdateableApps() { - return showAllUpdateableApps; - } - - public void toggleAllUpdateableApps() { - showAllUpdateableApps = !showAllUpdateableApps; - populateItems(); + //noinspection ComparatorCombinators + Collections.sort(appsToShowStatus, (o1, o2) -> o1.status.app.name.compareTo(o2.status.app.name)); } /** - * Completely rebuilds the underlying data structure used by this adapter. Note however, that - * this does not notify the recycler view of any changes. Thus, it is up to other methods which - * initiate a call to this method to make sure they appropriately notify the recyler view. + * Completely rebuilds the underlying data structure used by this adapter. */ private void populateItems() { items.clear(); - Set toShowStatusPackageNames = new HashSet<>(appsToShowStatus.size()); + Set toShowStatusUrls = new HashSet<>(appsToShowStatus.size()); for (AppStatus app : appsToShowStatus) { - toShowStatusPackageNames.add(app.status.app.packageName); + // Show status items.add(app); + toShowStatusUrls.add(app.status.getCanonicalUrl()); } - - if (updateableApps != null) { - // Only count/show apps which are not shown above in the "Apps to show status" list. - List updateableAppsToShow = new ArrayList<>(updateableApps.size()); - for (UpdateableApp app : updateableApps) { - if (!toShowStatusPackageNames.contains(app.app.packageName)) { - updateableAppsToShow.add(app); - } - } - - if (updateableAppsToShow.size() > 0) { - items.add(new UpdateableAppsHeader(activity, this, updateableAppsToShow)); - - if (showAllUpdateableApps) { - items.addAll(updateableAppsToShow); - } + // Show apps that are in state UpdateAvailable below the statuses + List updateableAppsToShow = new ArrayList<>(updateableApps.size()); + for (UpdateableApp app : updateableApps) { + if (!toShowStatusUrls.contains(app.apk.getCanonicalUrl())) { + updateableAppsToShow.add(app); } } - + if (updateableAppsToShow.size() > 0) { + // show header, if there's apps to update + items.add(new UpdateableAppsHeader(activity, this, updateableAppsToShow)); + // show all items, if "Show All" was clicked + if (showAllUpdateableApps) { + items.addAll(updateableAppsToShow); + } + } + // add vulnerable apps at the bottom items.addAll(knownVulnApps); } @@ -199,89 +221,24 @@ public class UpdatesAdapter extends RecyclerView.Adapter onCreateLoader(int id, Bundle args) { - Uri uri; - switch (id) { - case LOADER_CAN_UPDATE: - uri = AppProvider.getCanUpdateUri(); - break; - - case LOADER_KNOWN_VULN: - uri = AppProvider.getInstalledWithKnownVulnsUri(); - break; - - default: - throw new IllegalStateException("Unknown loader requested: " + id); - } - - return new CursorLoader( - activity, uri, Schema.AppMetadataTable.Cols.ALL, null, null, Schema.AppMetadataTable.Cols.NAME); - } - - @Override - public void onLoadFinished(@NonNull Loader loader, Cursor cursor) { - switch (loader.getId()) { - case LOADER_CAN_UPDATE: - onCanUpdateLoadFinished(cursor); - break; - - case LOADER_KNOWN_VULN: - onKnownVulnLoadFinished(cursor); - break; - } - - populateItems(); - notifyDataSetChanged(); - } - - private void onCanUpdateLoadFinished(Cursor cursor) { - updateableApps.clear(); - - cursor.moveToFirst(); - while (!cursor.isAfterLast()) { - updateableApps.add(new UpdateableApp(activity, new App(cursor))); - cursor.moveToNext(); - } - } - - private void onKnownVulnLoadFinished(Cursor cursor) { - knownVulnApps.clear(); - - cursor.moveToFirst(); - while (!cursor.isAfterLast()) { - knownVulnApps.add(new KnownVulnApp(activity, new App(cursor))); - cursor.moveToNext(); - } - } - - @Override - public void onLoaderReset(@NonNull Loader loader) { - } - /** * If this adapter is "active" then it is part of the current UI that the user is looking to. * Under those circumstances, we want to make sure it is up to date, and also listen to the * correct set of broadcasts. - * Doesn't listen for {@link AppUpdateStatusManager#BROADCAST_APPSTATUS_CHANGED} because the - * individual items in the recycler view will listen for the appropriate changes in state and - * update themselves accordingly (if they are displayed). */ public void setIsActive() { - appsToShowStatus.clear(); - populateAppStatuses(); - notifyDataSetChanged(); + loadUpdatableApps(); IntentFilter filter = new IntentFilter(); filter.addAction(AppUpdateStatusManager.BROADCAST_APPSTATUS_ADDED); filter.addAction(AppUpdateStatusManager.BROADCAST_APPSTATUS_REMOVED); + filter.addAction(AppUpdateStatusManager.BROADCAST_APPSTATUS_CHANGED); filter.addAction(AppUpdateStatusManager.BROADCAST_APPSTATUS_LIST_CHANGED); LocalBroadcastManager.getInstance(activity).registerReceiver(receiverAppStatusChanges, filter); } - public void stopListeningForStatusUpdates() { + void stopListeningForStatusUpdates() { LocalBroadcastManager.getInstance(activity).unregisterReceiver(receiverAppStatusChanges); } @@ -302,12 +259,7 @@ public class UpdatesAdapter extends RecyclerView.Adapter items, int position, @NonNull RecyclerView.ViewHolder holder, @NonNull List payloads) { AppStatus app = (AppStatus) items.get(position); - ((AppStatusListItemController) holder).bindModel(app.status.app); + ((AppStatusListItemController) holder).bindModel(app.status.app, app.status.apk, app.status); } } diff --git a/app/src/main/java/org/fdroid/fdroid/views/updates/items/AppStatusListItemController.java b/app/src/main/java/org/fdroid/fdroid/views/updates/items/AppStatusListItemController.java index 560d19c51..4c5920e64 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/updates/items/AppStatusListItemController.java +++ b/app/src/main/java/org/fdroid/fdroid/views/updates/items/AppStatusListItemController.java @@ -82,7 +82,7 @@ public class AppStatusListItemController extends AppListItemController { public void onClick(View view) { manager.addApk(appUpdateStatus.app, appUpdateStatus.apk, appUpdateStatus.status, appUpdateStatus.intent); - adapter.refreshStatuses(); + adapter.refreshItems(); } }).show(); break; @@ -90,7 +90,7 @@ public class AppStatusListItemController extends AppListItemController { } } - adapter.refreshStatuses(); + adapter.refreshItems(); } } diff --git a/app/src/main/java/org/fdroid/fdroid/views/updates/items/KnownVulnApp.java b/app/src/main/java/org/fdroid/fdroid/views/updates/items/KnownVulnApp.java index b79267a48..1e8024d0c 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/updates/items/KnownVulnApp.java +++ b/app/src/main/java/org/fdroid/fdroid/views/updates/items/KnownVulnApp.java @@ -5,6 +5,7 @@ import android.view.ViewGroup; import com.hannesdorfmann.adapterdelegates4.AdapterDelegate; import org.fdroid.fdroid.R; +import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.App; import java.util.List; @@ -24,18 +25,22 @@ import androidx.recyclerview.widget.RecyclerView; public class KnownVulnApp extends AppUpdateData { public final App app; + public final Apk apk; - public KnownVulnApp(AppCompatActivity activity, App app) { + public KnownVulnApp(AppCompatActivity activity, App app, Apk apk) { super(activity); this.app = app; + this.apk = apk; } public static class Delegate extends AdapterDelegate> { private final AppCompatActivity activity; + private final Runnable refreshApps; - public Delegate(AppCompatActivity activity) { + public Delegate(AppCompatActivity activity, Runnable refreshApps) { this.activity = activity; + this.refreshApps = refreshApps; } @Override @@ -46,7 +51,7 @@ public class KnownVulnApp extends AppUpdateData { @NonNull @Override protected RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) { - return new KnownVulnAppListItemController(activity, activity.getLayoutInflater() + return new KnownVulnAppListItemController(activity, refreshApps, activity.getLayoutInflater() .inflate(R.layout.known_vuln_app_list_item, parent, false)); } @@ -54,7 +59,7 @@ public class KnownVulnApp extends AppUpdateData { protected void onBindViewHolder(@NonNull List items, int position, @NonNull RecyclerView.ViewHolder holder, @NonNull List payloads) { KnownVulnApp app = (KnownVulnApp) items.get(position); - ((KnownVulnAppListItemController) holder).bindModel(app.app); + ((KnownVulnAppListItemController) holder).bindModel(app.app, app.apk, null); } } diff --git a/app/src/main/java/org/fdroid/fdroid/views/updates/items/KnownVulnAppListItemController.java b/app/src/main/java/org/fdroid/fdroid/views/updates/items/KnownVulnAppListItemController.java index 84242fc9e..b302402b0 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/updates/items/KnownVulnAppListItemController.java +++ b/app/src/main/java/org/fdroid/fdroid/views/updates/items/KnownVulnAppListItemController.java @@ -6,22 +6,15 @@ import android.content.Context; import android.content.Intent; import android.view.View; -import com.google.android.material.snackbar.Snackbar; - import org.fdroid.fdroid.AppUpdateStatusManager; import org.fdroid.fdroid.R; import org.fdroid.fdroid.data.Apk; -import org.fdroid.fdroid.data.ApkProvider; import org.fdroid.fdroid.data.App; -import org.fdroid.fdroid.data.AppPrefs; -import org.fdroid.fdroid.data.AppPrefsProvider; -import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.installer.InstallManagerService; import org.fdroid.fdroid.installer.Installer; import org.fdroid.fdroid.installer.InstallerService; import org.fdroid.fdroid.views.apps.AppListItemController; import org.fdroid.fdroid.views.apps.AppListItemState; -import org.fdroid.fdroid.views.updates.UpdatesAdapter; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -34,8 +27,11 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager; * (e.g. uninstall, update, disable). */ public class KnownVulnAppListItemController extends AppListItemController { - public KnownVulnAppListItemController(AppCompatActivity activity, View itemView) { + private final Runnable refreshApps; + + KnownVulnAppListItemController(AppCompatActivity activity, Runnable refreshApps, View itemView) { super(activity, itemView); + this.refreshApps = refreshApps; } @NonNull @@ -45,8 +41,7 @@ public class KnownVulnAppListItemController extends AppListItemController { String mainText; String actionButtonText; - Apk suggestedApk = ApkProvider.Helper.findSuggestedApk(activity, app); - if (shouldUpgradeInsteadOfUninstall(app, suggestedApk)) { + if (shouldUpgradeInsteadOfUninstall(app)) { mainText = activity.getString(R.string.updates__app_with_known_vulnerability__prompt_upgrade, app.name); actionButtonText = activity.getString(R.string.menu_upgrade); } else { @@ -56,30 +51,27 @@ public class KnownVulnAppListItemController extends AppListItemController { return new AppListItemState(app) .setMainText(mainText) - .showActionButton(actionButtonText) - .showSecondaryButton(activity.getString(R.string.updates__app_with_known_vulnerability__ignore)); + .showActionButton(actionButtonText); } - private boolean shouldUpgradeInsteadOfUninstall(@NonNull App app, @Nullable Apk suggestedApk) { - return suggestedApk != null && app.installedVersionCode < suggestedApk.versionCode; + private boolean shouldUpgradeInsteadOfUninstall(@NonNull App app) { + return app.installedVersionCode < app.autoInstallVersionCode; } @Override - protected void onActionButtonPressed(@NonNull App app) { - Apk installedApk = app.getInstalledApk(activity); + protected void onActionButtonPressed(@NonNull App app, Apk currentApk) { + Apk installedApk = app.installedApk; if (installedApk == null) { throw new IllegalStateException( "Tried to update or uninstall app with known vulnerability but it doesn't seem to be installed"); } - Apk suggestedApk = ApkProvider.Helper.findSuggestedApk(activity, app); - if (shouldUpgradeInsteadOfUninstall(app, suggestedApk)) { - LocalBroadcastManager manager = LocalBroadcastManager.getInstance(activity); + LocalBroadcastManager manager = LocalBroadcastManager.getInstance(activity); + if (shouldUpgradeInsteadOfUninstall(app)) { manager.registerReceiver(installReceiver, - Installer.getInstallIntentFilter(suggestedApk.getCanonicalUrl())); - InstallManagerService.queue(activity, app, suggestedApk); + Installer.getInstallIntentFilter(currentApk.getCanonicalUrl())); + InstallManagerService.queue(activity, app, currentApk); } else { - LocalBroadcastManager manager = LocalBroadcastManager.getInstance(activity); manager.registerReceiver(installReceiver, Installer.getUninstallIntentFilter(app.packageName)); InstallerService.uninstall(activity, installedApk); } @@ -87,41 +79,7 @@ public class KnownVulnAppListItemController extends AppListItemController { @Override public boolean canDismiss() { - return true; - } - - @Override - protected void onDismissApp(@NonNull final App app, UpdatesAdapter adapter) { - this.ignoreVulnerableApp(app); - } - - @Override - protected void onSecondaryButtonPressed(@NonNull App app) { - this.ignoreVulnerableApp(app); - } - - private void ignoreVulnerableApp(@NonNull final App app) { - setIgnoreVulnerableApp(app, true); - - Snackbar.make( - itemView, - R.string.app_list__dismiss_vulnerable_app, - Snackbar.LENGTH_LONG - ) - .setAction(R.string.undo, new View.OnClickListener() { - @Override - public void onClick(View view) { - setIgnoreVulnerableApp(app, false); - } - }) - .show(); - } - - private void setIgnoreVulnerableApp(@NonNull App app, boolean ignore) { - AppPrefs prefs = app.getPrefs(activity); - prefs.ignoreVulnerabilities = ignore; - AppPrefsProvider.Helper.update(activity, app, prefs); - refreshUpdatesList(); + return false; } private void unregisterInstallReceiver() { @@ -133,7 +91,7 @@ public class KnownVulnAppListItemController extends AppListItemController { * apps with known vulnerabilities (i.e. this app should no longer be in that list). */ private void refreshUpdatesList() { - activity.getContentResolver().notifyChange(AppProvider.getInstalledWithKnownVulnsUri(), null); + refreshApps.run(); } private final BroadcastReceiver installReceiver = new BroadcastReceiver() { diff --git a/app/src/main/java/org/fdroid/fdroid/views/updates/items/UpdateableApp.java b/app/src/main/java/org/fdroid/fdroid/views/updates/items/UpdateableApp.java index 916a93ad6..2ba92eeab 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/updates/items/UpdateableApp.java +++ b/app/src/main/java/org/fdroid/fdroid/views/updates/items/UpdateableApp.java @@ -5,6 +5,7 @@ import android.view.ViewGroup; import com.hannesdorfmann.adapterdelegates4.AdapterDelegate; import org.fdroid.fdroid.R; +import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.App; import java.util.List; @@ -24,10 +25,12 @@ import androidx.recyclerview.widget.RecyclerView; public class UpdateableApp extends AppUpdateData { public final App app; + public final Apk apk; - public UpdateableApp(AppCompatActivity activity, App app) { + public UpdateableApp(AppCompatActivity activity, App app, Apk apk) { super(activity); this.app = app; + this.apk = apk; } public static class Delegate extends AdapterDelegate> { @@ -54,7 +57,7 @@ public class UpdateableApp extends AppUpdateData { protected void onBindViewHolder(@NonNull List items, int position, @NonNull RecyclerView.ViewHolder holder, @NonNull List payloads) { UpdateableApp app = (UpdateableApp) items.get(position); - ((UpdateableAppListItemController) holder).bindModel(app.app); + ((UpdateableAppListItemController) holder).bindModel(app.app, app.apk, null); } } diff --git a/app/src/main/java/org/fdroid/fdroid/views/updates/items/UpdateableAppListItemController.java b/app/src/main/java/org/fdroid/fdroid/views/updates/items/UpdateableAppListItemController.java index 2731dc12b..ddcda1ca3 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/updates/items/UpdateableAppListItemController.java +++ b/app/src/main/java/org/fdroid/fdroid/views/updates/items/UpdateableAppListItemController.java @@ -4,11 +4,13 @@ import android.view.View; import com.google.android.material.snackbar.Snackbar; +import org.fdroid.database.AppPrefs; +import org.fdroid.database.AppPrefsDao; import org.fdroid.fdroid.AppUpdateStatusManager; import org.fdroid.fdroid.R; +import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.App; -import org.fdroid.fdroid.data.AppPrefs; -import org.fdroid.fdroid.data.AppPrefsProvider; +import org.fdroid.fdroid.data.DBHelper; import org.fdroid.fdroid.views.apps.AppListItemController; import org.fdroid.fdroid.views.apps.AppListItemState; import org.fdroid.fdroid.views.updates.UpdatesAdapter; @@ -16,6 +18,8 @@ import org.fdroid.fdroid.views.updates.UpdatesAdapter; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.Observer; /** * Very trimmed down list item. Only displays the app icon, name, and a download button. @@ -43,26 +47,35 @@ public class UpdateableAppListItemController extends AppListItemController { @Override protected void onDismissApp(@NonNull final App app, UpdatesAdapter adapter) { - final AppPrefs prefs = app.getPrefs(activity); - prefs.ignoreThisUpdate = app.autoInstallVersionCode; + AppPrefsDao appPrefsDao = DBHelper.getDb(activity).getAppPrefsDao(); + LiveData liveData = appPrefsDao.getAppPrefs(app.packageName); + liveData.observe(activity, new Observer() { + @Override + public void onChanged(org.fdroid.database.AppPrefs appPrefs) { + Utils.runOffUiThread(() -> { + AppPrefs newPrefs = appPrefs.toggleIgnoreVersionCodeUpdate(app.autoInstallVersionCode); + appPrefsDao.update(newPrefs); + return newPrefs; + }, newPrefs -> { + showUndoSnackBar(appPrefsDao, newPrefs); + AppUpdateStatusManager.getInstance(activity).checkForUpdates(); + }); + liveData.removeObserver(this); + } + }); + } + private void showUndoSnackBar(AppPrefsDao appPrefsDao, AppPrefs appPrefs) { Snackbar.make( itemView, R.string.app_list__dismiss_app_update, Snackbar.LENGTH_LONG ) - .setAction(R.string.undo, new View.OnClickListener() { - @Override - public void onClick(View view) { - prefs.ignoreThisUpdate = 0; - AppPrefsProvider.Helper.update(activity, app, prefs); - } - }) + .setAction(R.string.undo, view -> Utils.runOffUiThread(() -> { + AppPrefs newPrefs = appPrefs.toggleIgnoreVersionCodeUpdate(0); + appPrefsDao.update(newPrefs); + return true; + }, result -> AppUpdateStatusManager.getInstance(activity).checkForUpdates())) .show(); - - - // The act of updating here will trigger a re-query of the "can update" apps, so no need to do anything else - // to update the UI in response to this. - AppPrefsProvider.Helper.update(activity, app, prefs); } } diff --git a/app/src/main/res/layout/known_vuln_app_list_item.xml b/app/src/main/res/layout/known_vuln_app_list_item.xml index 138813a80..bc725b21b 100644 --- a/app/src/main/res/layout/known_vuln_app_list_item.xml +++ b/app/src/main/res/layout/known_vuln_app_list_item.xml @@ -24,6 +24,7 @@ tools:ignore="ContentDescription" />