[app] Make Updates tab use new DB

This commit is contained in:
Torsten Grote
2022-03-30 15:30:07 -03:00
committed by Hans-Christoph Steiner
parent adaf2a97ef
commit c4e92fba86
18 changed files with 368 additions and 467 deletions

View File

@@ -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);
}
}

View File

@@ -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);
}
/**

View File

@@ -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);
}
}

View File

@@ -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.

View File

@@ -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);
}

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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);
}
}
};
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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() {

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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"