mirror of
https://github.com/f-droid/fdroidclient.git
synced 2026-04-20 06:47:06 -04:00
[app] Make Updates tab use new DB
This commit is contained in:
committed by
Hans-Christoph Steiner
parent
dff7666cf3
commit
e6419b8f09
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Integer> 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<String, AppUpdateStatus> 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<Integer> 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<Pair<App, Apk>> apksToUpdate, Status status) {
|
||||
startBatchUpdates();
|
||||
for (Pair<App, Apk> 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<UpdatableApp> getUpdatableApps() {
|
||||
List<String> releaseChannels = Preferences.get().getBackendReleaseChannels();
|
||||
return updateChecker.getUpdatableApps(releaseChannels);
|
||||
}
|
||||
|
||||
private void addUpdatableApps(List<UpdatableApp> 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<UpdatableApp> 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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<App> 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<App> 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<String> 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<App> canUpdate) {
|
||||
if (canUpdate.size() > 0) {
|
||||
List<Pair<App, Apk>> 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<UpdatableApp> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<App>, 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<App>, 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.
|
||||
* <p>
|
||||
* Cases where an {@link Apk} will not be found in the database and for which we fall back to
|
||||
* the {@link InstalledAppProvider} include:
|
||||
* <li>System apps which are provided by a repository, but for which the version code bundled
|
||||
* with the system is not included in the repository.</li>
|
||||
* <li>Regular apps from a repository, where the installed version is old enough that it is no
|
||||
* longer available in the repository.</li>
|
||||
*/
|
||||
@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.
|
||||
|
||||
@@ -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.
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -43,7 +43,7 @@ class AppListAdapter extends RecyclerView.Adapter<StandardAppListItemController>
|
||||
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);
|
||||
|
||||
@@ -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<AppUpdateStatus> 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<AppUpdateStatus> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ public class InstalledAppListAdapter extends RecyclerView.Adapter<InstalledAppLi
|
||||
}
|
||||
|
||||
cursor.moveToPosition(position);
|
||||
holder.bindModel(new App(cursor));
|
||||
holder.bindModel(new App(cursor), null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -23,10 +23,8 @@
|
||||
package org.fdroid.fdroid.views.main;
|
||||
|
||||
import android.app.SearchManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
@@ -38,7 +36,6 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
@@ -46,7 +43,6 @@ import com.google.android.material.badge.BadgeDrawable;
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
||||
|
||||
import org.fdroid.fdroid.AppUpdateStatusManager;
|
||||
import org.fdroid.fdroid.AppUpdateStatusManager.AppUpdateStatus;
|
||||
import org.fdroid.fdroid.FDroidApp;
|
||||
import org.fdroid.fdroid.NfcHelper;
|
||||
import org.fdroid.fdroid.Preferences;
|
||||
@@ -59,7 +55,6 @@ import org.fdroid.fdroid.nearby.SwapService;
|
||||
import org.fdroid.fdroid.nearby.SwapWorkflowActivity;
|
||||
import org.fdroid.fdroid.nearby.TreeUriScannerIntentService;
|
||||
import org.fdroid.fdroid.nearby.WifiStateChangeService;
|
||||
import org.fdroid.fdroid.net.DownloaderService;
|
||||
import org.fdroid.fdroid.views.AppDetailsActivity;
|
||||
import org.fdroid.fdroid.views.ManageReposActivity;
|
||||
import org.fdroid.fdroid.views.apps.AppListActivity;
|
||||
@@ -137,13 +132,10 @@ public class MainActivity extends AppCompatActivity {
|
||||
updatesBadge = bottomNavigation.getOrCreateBadge(R.id.updates);
|
||||
updatesBadge.setVisible(false);
|
||||
|
||||
IntentFilter updateableAppsFilter = new IntentFilter(AppUpdateStatusManager.BROADCAST_APPSTATUS_LIST_CHANGED);
|
||||
updateableAppsFilter.addAction(AppUpdateStatusManager.BROADCAST_APPSTATUS_CHANGED);
|
||||
updateableAppsFilter.addAction(AppUpdateStatusManager.BROADCAST_APPSTATUS_REMOVED);
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(onUpdateableAppsChanged, updateableAppsFilter);
|
||||
|
||||
initialRepoUpdateIfRequired();
|
||||
|
||||
AppUpdateStatusManager.getInstance(this).getNumUpdatableApps().observe(this, this::refreshUpdatesBadge);
|
||||
|
||||
Intent intent = getIntent();
|
||||
handleSearchOrAppViewIntent(intent);
|
||||
}
|
||||
@@ -368,7 +360,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private void refreshUpdatesBadge(int canUpdateCount) {
|
||||
if (canUpdateCount == 0) {
|
||||
if (canUpdateCount <= 0) {
|
||||
updatesBadge.setVisible(false);
|
||||
updatesBadge.clearNumber();
|
||||
} else {
|
||||
@@ -393,62 +385,4 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* There are a bunch of reasons why we would get notified about app statuses.
|
||||
* The ones we are interested in are those which would result in the "items requiring user interaction"
|
||||
* to increase or decrease:
|
||||
* * Change in status to:
|
||||
* * {@link AppUpdateStatusManager.Status#ReadyToInstall} (Causes the count to go UP by one)
|
||||
* * {@link AppUpdateStatusManager.Status#Installed} (Causes the count to go DOWN by one)
|
||||
*/
|
||||
private final BroadcastReceiver onUpdateableAppsChanged = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
boolean updateBadge = false;
|
||||
|
||||
AppUpdateStatusManager manager = AppUpdateStatusManager.getInstance(context);
|
||||
|
||||
String reason = intent.getStringExtra(AppUpdateStatusManager.EXTRA_REASON_FOR_CHANGE);
|
||||
switch (intent.getAction()) {
|
||||
// Apps which are added/removed from the list due to becoming ready to install or a repo being
|
||||
// disabled both cause us to increase/decrease our badge count respectively.
|
||||
case AppUpdateStatusManager.BROADCAST_APPSTATUS_LIST_CHANGED:
|
||||
if (AppUpdateStatusManager.REASON_READY_TO_INSTALL.equals(reason) ||
|
||||
AppUpdateStatusManager.REASON_REPO_DISABLED.equals(reason)) {
|
||||
updateBadge = true;
|
||||
}
|
||||
break;
|
||||
|
||||
// Apps which were previously "Ready to install" but have been removed. We need to lower our badge
|
||||
// count in response to this.
|
||||
case AppUpdateStatusManager.BROADCAST_APPSTATUS_REMOVED:
|
||||
AppUpdateStatus status = intent.getParcelableExtra(AppUpdateStatusManager.EXTRA_STATUS);
|
||||
if (status != null && status.status == AppUpdateStatusManager.Status.ReadyToInstall) {
|
||||
updateBadge = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if we have moved into the ReadyToInstall or Installed state.
|
||||
AppUpdateStatus status = manager.get(
|
||||
intent.getStringExtra(DownloaderService.EXTRA_CANONICAL_URL));
|
||||
boolean isStatusChange = intent.getBooleanExtra(AppUpdateStatusManager.EXTRA_IS_STATUS_UPDATE, false);
|
||||
if (isStatusChange
|
||||
&& status != null
|
||||
&& (status.status == AppUpdateStatusManager.Status.ReadyToInstall || status.status == AppUpdateStatusManager.Status.Installed)) { // NOCHECKSTYLE LineLength
|
||||
updateBadge = true;
|
||||
}
|
||||
|
||||
if (updateBadge) {
|
||||
int count = 0;
|
||||
for (AppUpdateStatus s : manager.getAll()) {
|
||||
if (s.status == AppUpdateStatusManager.Status.ReadyToInstall) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
refreshUpdatesBadge(count);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,20 +1,22 @@
|
||||
package org.fdroid.fdroid.views.updates;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.hannesdorfmann.adapterdelegates4.AdapterDelegatesManager;
|
||||
|
||||
import org.fdroid.fdroid.Preferences;
|
||||
import org.fdroid.database.FDroidDatabase;
|
||||
import org.fdroid.database.UpdatableApp;
|
||||
import org.fdroid.database.DbUpdateChecker;
|
||||
import org.fdroid.fdroid.AppUpdateStatusManager;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
import org.fdroid.fdroid.data.Schema;
|
||||
import org.fdroid.fdroid.data.DBHelper;
|
||||
import org.fdroid.fdroid.views.updates.items.AppStatus;
|
||||
import org.fdroid.fdroid.views.updates.items.AppUpdateData;
|
||||
import org.fdroid.fdroid.views.updates.items.KnownVulnApp;
|
||||
@@ -23,19 +25,21 @@ import org.fdroid.fdroid.views.updates.items.UpdateableAppsHeader;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.CursorLoader;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
/**
|
||||
* Manages the following types of information:
|
||||
* <ul>
|
||||
@@ -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).
|
||||
* <p>
|
||||
* 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<RecyclerView.ViewHolder>
|
||||
implements LoaderManager.LoaderCallbacks<Cursor> {
|
||||
|
||||
private static final int LOADER_CAN_UPDATE = 289753982;
|
||||
private static final int LOADER_KNOWN_VULN = 520389740;
|
||||
public class UpdatesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
|
||||
private final AdapterDelegatesManager<List<AppUpdateData>> delegatesManager = new AdapterDelegatesManager<>();
|
||||
private final List<AppUpdateData> items = new ArrayList<>();
|
||||
|
||||
private final AppCompatActivity activity;
|
||||
private final DbUpdateChecker updateChecker;
|
||||
|
||||
private final List<AppUpdateData> items = new ArrayList<>();
|
||||
private final List<AppStatus> appsToShowStatus = new ArrayList<>();
|
||||
private final List<UpdateableApp> updateableApps = new ArrayList<>();
|
||||
private final List<KnownVulnApp> 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<RecyclerView.ViewHolder
|
||||
delegatesManager.addDelegate(new AppStatus.Delegate(activity))
|
||||
.addDelegate(new UpdateableApp.Delegate(activity))
|
||||
.addDelegate(new UpdateableAppsHeader.Delegate(activity))
|
||||
.addDelegate(new KnownVulnApp.Delegate(activity));
|
||||
.addDelegate(new KnownVulnApp.Delegate(activity, this::loadUpdatableApps));
|
||||
|
||||
initLoaders();
|
||||
FDroidDatabase db = DBHelper.getDb(activity);
|
||||
updateChecker = new DbUpdateChecker(db, activity.getPackageManager());
|
||||
loadUpdatableApps();
|
||||
}
|
||||
|
||||
private void loadUpdatableApps() {
|
||||
List<String> 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<RecyclerView.ViewHolder
|
||||
status.status == AppUpdateStatusManager.Status.ReadyToInstall;
|
||||
}
|
||||
|
||||
private void onCanUpdateLoadFinished(List<UpdatableApp> 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<AppStatus>() {
|
||||
@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<String> toShowStatusPackageNames = new HashSet<>(appsToShowStatus.size());
|
||||
Set<String> 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<UpdateableApp> 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<UpdateableApp> 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<RecyclerView.ViewHolder
|
||||
delegatesManager.onBindViewHolder(items, position, holder);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Cursor> 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<Cursor> 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<Cursor> 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<RecyclerView.ViewHolder
|
||||
* We need to rerun our database query to get a list of apps to update.
|
||||
*/
|
||||
private void onUpdateableAppsChanged() {
|
||||
initLoaders();
|
||||
}
|
||||
|
||||
private void initLoaders() {
|
||||
activity.getSupportLoaderManager().initLoader(LOADER_CAN_UPDATE, null, this);
|
||||
activity.getSupportLoaderManager().initLoader(LOADER_KNOWN_VULN, null, this);
|
||||
loadUpdatableApps();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -315,20 +267,15 @@ public class UpdatesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
||||
* some which are ready to install.
|
||||
*/
|
||||
private void onFoundAppsReadyToInstall() {
|
||||
populateAppStatuses();
|
||||
notifyDataSetChanged();
|
||||
refreshItems();
|
||||
}
|
||||
|
||||
private void onAppStatusAdded() {
|
||||
appsToShowStatus.clear();
|
||||
populateAppStatuses();
|
||||
notifyDataSetChanged();
|
||||
refreshItems();
|
||||
}
|
||||
|
||||
private void onAppStatusRemoved() {
|
||||
appsToShowStatus.clear();
|
||||
populateAppStatuses();
|
||||
notifyDataSetChanged();
|
||||
loadUpdatableApps();
|
||||
}
|
||||
|
||||
private final BroadcastReceiver receiverAppStatusChanges = new BroadcastReceiver() {
|
||||
@@ -338,6 +285,12 @@ public class UpdatesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
||||
return;
|
||||
}
|
||||
switch (intent.getAction()) {
|
||||
case AppUpdateStatusManager.BROADCAST_APPSTATUS_CHANGED:
|
||||
if (intent.getBooleanExtra(AppUpdateStatusManager.EXTRA_IS_STATUS_UPDATE, false)) {
|
||||
refreshItems();
|
||||
}
|
||||
break;
|
||||
|
||||
case AppUpdateStatusManager.BROADCAST_APPSTATUS_LIST_CHANGED:
|
||||
onManyAppStatusesChanged(intent.getStringExtra(AppUpdateStatusManager.EXTRA_REASON_FOR_CHANGE));
|
||||
break;
|
||||
@@ -353,11 +306,4 @@ public class UpdatesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* If an item representing an {@link org.fdroid.fdroid.AppUpdateStatusManager.AppUpdateStatus} is dismissed,
|
||||
* then we should rebuild the list of app statuses and update the adapter.
|
||||
*/
|
||||
public void refreshStatuses() {
|
||||
onAppStatusRemoved();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ public class AppStatus extends AppUpdateData {
|
||||
protected void onBindViewHolder(@NonNull List<AppUpdateData> items, int position,
|
||||
@NonNull RecyclerView.ViewHolder holder, @NonNull List<Object> payloads) {
|
||||
AppStatus app = (AppStatus) items.get(position);
|
||||
((AppStatusListItemController) holder).bindModel(app.status.app);
|
||||
((AppStatusListItemController) holder).bindModel(app.status.app, app.status.apk, app.status);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<List<AppUpdateData>> {
|
||||
|
||||
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<AppUpdateData> items, int position,
|
||||
@NonNull RecyclerView.ViewHolder holder, @NonNull List<Object> payloads) {
|
||||
KnownVulnApp app = (KnownVulnApp) items.get(position);
|
||||
((KnownVulnAppListItemController) holder).bindModel(app.app);
|
||||
((KnownVulnAppListItemController) holder).bindModel(app.app, app.apk, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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<List<AppUpdateData>> {
|
||||
@@ -54,7 +57,7 @@ public class UpdateableApp extends AppUpdateData {
|
||||
protected void onBindViewHolder(@NonNull List<AppUpdateData> items, int position,
|
||||
@NonNull RecyclerView.ViewHolder holder, @NonNull List<Object> payloads) {
|
||||
UpdateableApp app = (UpdateableApp) items.get(position);
|
||||
((UpdateableAppListItemController) holder).bindModel(app.app);
|
||||
((UpdateableAppListItemController) holder).bindModel(app.app, app.apk, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<AppPrefs> liveData = appPrefsDao.getAppPrefs(app.packageName);
|
||||
liveData.observe(activity, new Observer<AppPrefs>() {
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/badge"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:layout_width="24dp"
|
||||
@@ -43,7 +44,7 @@
|
||||
android:textSize="16sp"
|
||||
android:textColor="?attr/installedApps"
|
||||
android:ellipsize="end"
|
||||
app:layout_constraintStart_toEndOf="@+id/icon"
|
||||
app:layout_constraintStart_toEndOf="@+id/badge"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:layout_marginStart="8dp"
|
||||
|
||||
Reference in New Issue
Block a user