From d97d995c43917fefe57a8ef8a33367ec2d2089a8 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 17 Mar 2022 15:35:01 -0300 Subject: [PATCH] [app] Use new IndexV1Updater and make latest and category tab use new DB --- app/build.gradle | 1 + .../views/main/CategoriesViewBinder.java | 72 ++------ .../java/org/fdroid/fdroid/FDroidApp.java | 40 +++- .../java/org/fdroid/fdroid/IndexUpdater.java | 20 +- .../org/fdroid/fdroid/IndexV1Updater.java | 39 ++-- .../java/org/fdroid/fdroid/UpdateService.java | 111 ++++++----- .../fdroid/fdroid/UpdateServiceListener.java | 28 +++ .../main/java/org/fdroid/fdroid/Utils.java | 80 +++++++- .../data/InstalledAppProviderService.java | 1 + .../fdroid/fdroid/net/DownloaderFactory.java | 34 ++-- .../fdroid/fdroid/net/DownloaderService.java | 16 +- .../receiver/PackageManagerReceiver.java | 2 + .../org/fdroid/fdroid/views/StatusBanner.java | 13 +- .../views/categories/AppCardController.java | 26 ++- .../views/categories/AppPreviewAdapter.java | 19 +- .../views/categories/CategoryAdapter.java | 23 ++- .../views/categories/CategoryController.java | 155 +++++----------- .../fdroid/views/main/LatestAdapter.java | 22 ++- .../fdroid/views/main/LatestViewBinder.java | 173 +++++++++--------- 19 files changed, 463 insertions(+), 412 deletions(-) create mode 100644 app/src/main/java/org/fdroid/fdroid/UpdateServiceListener.java diff --git a/app/build.gradle b/app/build.gradle index fdff2a796..bad8ac74b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -143,6 +143,7 @@ android { dependencies { implementation project(":libs:download") + implementation project(":libs:database") implementation 'androidx.appcompat:appcompat:1.4.2' implementation 'androidx.preference:preference:1.2.0' implementation 'androidx.gridlayout:gridlayout:1.0.0' diff --git a/app/src/full/java/org/fdroid/fdroid/views/main/CategoriesViewBinder.java b/app/src/full/java/org/fdroid/fdroid/views/main/CategoriesViewBinder.java index 1a65a72c3..032032de2 100644 --- a/app/src/full/java/org/fdroid/fdroid/views/main/CategoriesViewBinder.java +++ b/app/src/full/java/org/fdroid/fdroid/views/main/CategoriesViewBinder.java @@ -1,20 +1,19 @@ package org.fdroid.fdroid.views.main; import android.content.Intent; -import android.database.Cursor; -import android.os.Bundle; import android.view.View; import android.widget.FrameLayout; import android.widget.TextView; import com.google.android.material.floatingactionbutton.FloatingActionButton; +import org.fdroid.database.Category; +import org.fdroid.database.FDroidDatabase; +import org.fdroid.database.FDroidDatabaseHolder; import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; import org.fdroid.fdroid.UpdateService; import org.fdroid.fdroid.Utils; -import org.fdroid.fdroid.data.CategoryProvider; -import org.fdroid.fdroid.data.Schema; import org.fdroid.fdroid.panic.HidingManager; import org.fdroid.fdroid.views.apps.AppListActivity; import org.fdroid.fdroid.views.categories.CategoryAdapter; @@ -25,11 +24,9 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; -import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; -import androidx.loader.app.LoaderManager; -import androidx.loader.content.CursorLoader; -import androidx.loader.content.Loader; +import androidx.lifecycle.Observer; +import androidx.lifecycle.Transformations; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; @@ -39,11 +36,9 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; * Will start a loader to get the list of categories from the database and populate a recycler * view with relevant info about each. */ -class CategoriesViewBinder implements LoaderManager.LoaderCallbacks { +class CategoriesViewBinder implements Observer> { public static final String TAG = "CategoriesViewBinder"; - private static final int LOADER_ID = 429820532; - private final CategoryAdapter categoryAdapter; private final AppCompatActivity activity; private final TextView emptyState; @@ -51,10 +46,12 @@ class CategoriesViewBinder implements LoaderManager.LoaderCallbacks { CategoriesViewBinder(final AppCompatActivity activity, FrameLayout parent) { this.activity = activity; + FDroidDatabase db = FDroidDatabaseHolder.getDb(activity); + Transformations.distinctUntilChanged(db.getRepositoryDao().getLiveCategories()).observe(activity, this); View categoriesView = activity.getLayoutInflater().inflate(R.layout.main_tab_categories, parent, true); - categoryAdapter = new CategoryAdapter(activity, activity.getSupportLoaderManager()); + categoryAdapter = new CategoryAdapter(activity, db); emptyState = (TextView) categoriesView.findViewById(R.id.empty_state); @@ -92,49 +89,17 @@ class CategoriesViewBinder implements LoaderManager.LoaderCallbacks { } } }); - - activity.getSupportLoaderManager().restartLoader(LOADER_ID, null, this); - } - - @NonNull - @Override - public Loader onCreateLoader(int id, Bundle args) { - if (id != LOADER_ID) { - throw new IllegalArgumentException("id != LOADER_ID"); - } - - return new CursorLoader( - activity, - CategoryProvider.getAllCategories(), - Schema.CategoryTable.Cols.ALL, - null, - null, - null - ); } /** - * Reads all categories from the cursor and stores them in memory to provide to the {@link CategoryAdapter}. - *

- * It does this so it is easier to deal with localized/unlocalized categories without having - * to store the localized version in the database. It is not expected that the list of categories - * will grow so large as to make this a performance concern. If it does in the future, the - * {@link CategoryAdapter} can be reverted to wrap the cursor again, and localized category - * names can be stored in the database (allowing sorting in their localized form). + * Gets all categories from the DB and stores them in memory to provide to the {@link CategoryAdapter}. */ @Override - public void onLoadFinished(Loader loader, Cursor cursor) { - if (loader.getId() != LOADER_ID || cursor == null) { - return; + public void onChanged(List categories) { + List categoryNames = new ArrayList<>(categories.size()); + for (Category c : categories) { + categoryNames.add(c.getId()); } - - List categoryNames = new ArrayList<>(cursor.getCount()); - cursor.moveToFirst(); - while (!cursor.isAfterLast()) { - categoryNames.add(cursor.getString(cursor.getColumnIndex(Schema.CategoryTable.Cols.NAME))); - cursor.moveToNext(); - } - Collections.sort(categoryNames, new Comparator() { @Override public int compare(String categoryOne, String categoryTwo) { @@ -155,13 +120,4 @@ class CategoriesViewBinder implements LoaderManager.LoaderCallbacks { } } - @Override - public void onLoaderReset(Loader loader) { - if (loader.getId() != LOADER_ID) { - return; - } - - categoryAdapter.setCategories(Collections.emptyList()); - } - } diff --git a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java index b6a290916..66d11840f 100644 --- a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java +++ b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java @@ -33,6 +33,7 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Configuration; +import android.content.res.Resources; import android.net.Uri; import android.os.Build; import android.os.Environment; @@ -51,12 +52,15 @@ import org.acra.config.CoreConfigurationBuilder; import org.acra.config.DialogConfigurationBuilder; import org.acra.config.MailSenderConfigurationBuilder; import org.apache.commons.net.util.SubnetUtils; +import org.fdroid.database.FDroidDatabase; +import org.fdroid.database.FDroidDatabaseHolder; +import org.fdroid.database.Repository; import org.fdroid.fdroid.Preferences.ChangeListener; import org.fdroid.fdroid.Preferences.Theme; import org.fdroid.fdroid.compat.PRNGFixes; import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.AppProvider; -import org.fdroid.fdroid.data.InstalledAppProviderService; +import org.fdroid.fdroid.data.DBHelper; import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.installer.ApkFileProvider; import org.fdroid.fdroid.installer.InstallHistoryService; @@ -79,8 +83,13 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatDelegate; import androidx.core.content.ContextCompat; +import androidx.core.os.ConfigurationCompat; +import androidx.core.os.LocaleListCompat; + import info.guardianproject.netcipher.NetCipher; import info.guardianproject.netcipher.proxy.OrbotHelper; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.schedulers.Schedulers; public class FDroidApp extends Application implements androidx.work.Configuration.Provider { @@ -100,6 +109,8 @@ public class FDroidApp extends Application implements androidx.work.Configuratio public static volatile String bssid; public static volatile Repo repo = new Repo(); + public static volatile List repos; + public static volatile int networkState = ConnectivityMonitorService.FLAG_NET_UNAVAILABLE; public static final SubnetUtils.SubnetInfo UNSET_SUBNET_INFO = new SubnetUtils("0.0.0.0/32").getInfo(); @@ -241,7 +252,15 @@ public class FDroidApp extends Application implements androidx.work.Configuratio currentLocale = newConfig.getLocales().toString(); } if (!TextUtils.equals(lastLocale, currentLocale)) { - UpdateService.forceUpdateRepo(this); + FDroidDatabase db = DBHelper.getDb(this.getApplicationContext()); + Single.fromCallable(() -> { + long now = System.currentTimeMillis(); + LocaleListCompat locales = + ConfigurationCompat.getLocales(Resources.getSystem().getConfiguration()); + db.afterLocalesChanged(locales); + Log.d(TAG, "Updating DB locales took: " + (System.currentTimeMillis() - now) + "ms"); + return true; + }).subscribeOn(Schedulers.io()).subscribe(); } atStartTime.edit().putString(lastLocaleKey, currentLocale).apply(); } @@ -316,6 +335,11 @@ public class FDroidApp extends Application implements androidx.work.Configuratio } } + // keep a static copy of the repositories around and in-sync + // not how one would normally do this, but it is a common pattern in this codebase + FDroidDatabase db = FDroidDatabaseHolder.getDb(this); + db.getRepositoryDao().getLiveRepositories().observeForever(repositories -> repos = repositories); + PRNGFixes.apply(); applyTheme(); @@ -331,7 +355,8 @@ public class FDroidApp extends Application implements androidx.work.Configuratio preferences.setForceOldIndex(true); } - InstalledAppProviderService.compareToPackageManager(this); + // TODO should not be needed anymore + //InstalledAppProviderService.compareToPackageManager(this); // If the user changes the preference to do with filtering anti-feature apps, // it is easier to just notify a change in the app provider, @@ -525,6 +550,15 @@ public class FDroidApp extends Application implements androidx.work.Configuratio } } + @Nullable + public static Repository getRepo(long repoId) { + if (repos == null) return null; + for (Repository r : repos) { + if (r.getRepoId() == repoId) return r; + } + return null; + } + public static Context getInstance() { return instance; } diff --git a/app/src/main/java/org/fdroid/fdroid/IndexUpdater.java b/app/src/main/java/org/fdroid/fdroid/IndexUpdater.java index 32d8f956a..8d041631a 100644 --- a/app/src/main/java/org/fdroid/fdroid/IndexUpdater.java +++ b/app/src/main/java/org/fdroid/fdroid/IndexUpdater.java @@ -28,7 +28,6 @@ import android.content.ContentValues; import android.content.Context; import android.content.pm.PackageInfo; import android.content.res.Resources.NotFoundException; -import android.net.Uri; import android.text.TextUtils; import android.util.Log; import android.util.Pair; @@ -46,7 +45,6 @@ import org.fdroid.fdroid.data.RepoXMLHandler; import org.fdroid.fdroid.data.Schema.RepoTable; import org.fdroid.fdroid.installer.InstallManagerService; import org.fdroid.fdroid.installer.InstallerService; -import org.fdroid.fdroid.net.DownloaderFactory; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; @@ -134,10 +132,11 @@ public class IndexUpdater { try { destFile = File.createTempFile("dl-", "", context.getCacheDir()); destFile.deleteOnExit(); // this probably does nothing, but maybe... - downloader = DownloaderFactory.createWithTryFirstMirror(repo, Uri.parse(indexUrl), destFile); - downloader.setCacheTag(repo.lastetag); - downloader.setListener(downloadListener); - downloader.download(); + // TODO we don't use this anymore + // downloader = DownloaderFactory.createWithTryFirstMirror(repo, Uri.parse(indexUrl), destFile); + // downloader.setCacheTag(repo.lastetag); + // downloader.setListener(downloadListener); + // downloader.download(); } catch (IOException e) { if (destFile != null) { @@ -147,9 +146,6 @@ public class IndexUpdater { } throw new UpdateException(repo, "Error getting F-Droid index file", e); - } catch (InterruptedException e) { - // ignored if canceled, the local database just won't be updated - e.printStackTrace(); } // TODO is it safe to delete destFile in finally block? return new Pair<>(downloader, destFile); } @@ -255,19 +251,19 @@ public class IndexUpdater { protected final ProgressListener downloadListener = new ProgressListener() { @Override public void onProgress(long bytesRead, long totalBytes) { - UpdateService.reportDownloadProgress(context, IndexUpdater.this, bytesRead, totalBytes); + UpdateService.reportDownloadProgress(context, indexUrl, bytesRead, totalBytes); } }; protected final ProgressListener processIndexListener = new ProgressListener() { @Override public void onProgress(long bytesRead, long totalBytes) { - UpdateService.reportProcessIndexProgress(context, IndexUpdater.this, bytesRead, totalBytes); + UpdateService.reportProcessIndexProgress(context, indexUrl, bytesRead, totalBytes); } }; protected void notifyProcessingApps(int appsSaved, int totalApps) { - UpdateService.reportProcessingAppsProgress(context, this, appsSaved, totalApps); + UpdateService.reportProcessingAppsProgress(context, indexUrl, appsSaved, totalApps); } protected void notifyCommittingToDb() { diff --git a/app/src/main/java/org/fdroid/fdroid/IndexV1Updater.java b/app/src/main/java/org/fdroid/fdroid/IndexV1Updater.java index 359a7ebad..cca009960 100644 --- a/app/src/main/java/org/fdroid/fdroid/IndexV1Updater.java +++ b/app/src/main/java/org/fdroid/fdroid/IndexV1Updater.java @@ -25,7 +25,6 @@ package org.fdroid.fdroid; import android.content.ContentValues; import android.content.Context; import android.content.pm.PackageInfo; -import android.net.Uri; import android.text.TextUtils; import android.util.Log; @@ -41,7 +40,6 @@ import com.fasterxml.jackson.databind.InjectableValues; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.io.FileUtils; -import org.fdroid.download.Downloader; import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.Repo; @@ -49,7 +47,6 @@ import org.fdroid.fdroid.data.RepoPersister; import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.data.RepoPushRequest; import org.fdroid.fdroid.data.Schema; -import org.fdroid.fdroid.net.DownloaderFactory; import java.io.File; import java.io.IOException; @@ -63,7 +60,6 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.jar.JarEntry; -import java.util.jar.JarFile; /** * Receives the index data about all available apps and packages via the V1 @@ -105,43 +101,42 @@ public class IndexV1Updater extends IndexUpdater { @Override public boolean update() throws IndexUpdater.UpdateException { File destFile = null; - Downloader downloader; + // Downloader downloader; try { destFile = File.createTempFile("dl-", "", context.getCacheDir()); destFile.deleteOnExit(); // this probably does nothing, but maybe... + // TODO we don't use that anymore // read file name from file - downloader = DownloaderFactory.createWithTryFirstMirror(repo, Uri.parse(indexUrl), destFile); - downloader.setCacheTag(repo.lastetag); - downloader.setListener(downloadListener); - downloader.download(); - hasChanged = downloader.hasChanged(); + // downloader = DownloaderFactory.createWithTryFirstMirror(repo, Uri.parse(indexUrl), destFile); + // downloader.setCacheTag(repo.lastetag); + // downloader.setListener(downloadListener); + // downloader.download(); + // hasChanged = downloader.hasChanged(); if (!hasChanged) { return true; } - processDownloadedIndex(destFile, downloader.getCacheTag()); + // processDownloadedIndex(destFile, downloader.getCacheTag()); } catch (IOException e) { if (destFile != null) { FileUtils.deleteQuietly(destFile); } throw new IndexUpdater.UpdateException(repo, "Error getting F-Droid index file", e); - } catch (InterruptedException e) { - // ignored if canceled, the local database just won't be updated } // TODO is it safe to delete destFile in finally block? return true; } - private void processDownloadedIndex(File outputFile, String cacheTag) - throws IOException, IndexUpdater.UpdateException { - JarFile jarFile = new JarFile(outputFile, true); - JarEntry indexEntry = (JarEntry) jarFile.getEntry(DATA_FILE_NAME); - InputStream indexInputStream = new ProgressBufferedInputStream(jarFile.getInputStream(indexEntry), - processIndexListener, (int) indexEntry.getSize()); - processIndexV1(indexInputStream, indexEntry, cacheTag); - jarFile.close(); - } + //private void processDownloadedIndex(File outputFile, String cacheTag) + // throws IOException, IndexUpdater.UpdateException { + // JarFile jarFile = new JarFile(outputFile, true); + // JarEntry indexEntry = (JarEntry) jarFile.getEntry(DATA_FILE_NAME); + // InputStream indexInputStream = new ProgressBufferedInputStream(jarFile.getInputStream(indexEntry), + // processIndexListener, (int) indexEntry.getSize()); + // processIndexV1(indexInputStream, indexEntry, cacheTag); + // jarFile.close(); + //} /** * Get the standard {@link ObjectMapper} instance used for parsing {@code index-v1.json}. diff --git a/app/src/main/java/org/fdroid/fdroid/UpdateService.java b/app/src/main/java/org/fdroid/fdroid/UpdateService.java index 16146e9c0..6962e2add 100644 --- a/app/src/main/java/org/fdroid/fdroid/UpdateService.java +++ b/app/src/main/java/org/fdroid/fdroid/UpdateService.java @@ -37,21 +37,27 @@ 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.InstalledAppProviderService; -import org.fdroid.fdroid.data.Repo; -import org.fdroid.fdroid.data.RepoProvider; 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.Objects; import java.util.concurrent.TimeUnit; import androidx.annotation.NonNull; @@ -250,28 +256,19 @@ public class UpdateService extends JobIntentService { } } - /** - * Return a {@link List} of all {@link Repo}s that have either a local - * canonical URL or a local mirror URL. These are repos that can be - * updated and used without using the Internet. - */ - public static List getLocalRepos(Context context) { - return getLocalRepos(RepoProvider.Helper.all(context)); - } - /** * Return the repos in the {@code repos} {@link List} that have either a * local canonical URL or a local mirror URL. These are repos that can be * updated and used without using the Internet. */ - public static List getLocalRepos(List repos) { - ArrayList localRepos = new ArrayList<>(); - for (Repo repo : repos) { - if (isLocalRepoAddress(repo.address)) { + public static List getLocalRepos(List repos) { + ArrayList localRepos = new ArrayList<>(); + for (Repository repo : repos) { + if (isLocalRepoAddress(repo.getAddress())) { localRepos.add(repo); } else { - for (String mirrorAddress : repo.getMirrorList()) { - if (isLocalRepoAddress(mirrorAddress)) { + for (Mirror mirror : repo.getMirrors()) { + if (!mirror.isHttp()) { localRepos.add(repo); break; } @@ -422,10 +419,10 @@ public class UpdateService extends JobIntentService { try { final Preferences fdroidPrefs = Preferences.get(); - - // Grab some preliminary information, then we can release the - // database while we do all the downloading, etc... - List repos = RepoProvider.Helper.all(this); + // always get repos fresh from DB, because + // * when an update is requested early at app start, the repos above might not be available, yet + // * when an update is requested when adding a new repo, it might not be in the FDroidApp list, yet + List repos = db.getRepositoryDao().getRepositories(); // See if it's time to actually do anything yet... int netState = ConnectivityMonitorService.getNetworkState(this); @@ -433,7 +430,7 @@ public class UpdateService extends JobIntentService { Utils.debugLog(TAG, "skipping internet check, this is local: " + address); } else if (netState == ConnectivityMonitorService.FLAG_NET_UNAVAILABLE) { // keep track of repos that have a local copy in case internet is not available - List localRepos = getLocalRepos(repos); + List localRepos = getLocalRepos(repos); if (localRepos.size() > 0) { repos = localRepos; } else { @@ -447,7 +444,7 @@ public class UpdateService extends JobIntentService { Utils.debugLog(TAG, "manually requested or forced update"); if (forcedUpdate) { DBHelper.resetTransient(this); - InstalledAppProviderService.compareToPackageManager(this); + // InstalledAppProviderService.compareToPackageManager(this); } } else if (!fdroidPrefs.isBackgroundDownloadAllowed() && !fdroidPrefs.isOnDemandDownloadAllowed()) { Utils.debugLog(TAG, "don't run update"); @@ -464,34 +461,33 @@ public class UpdateService extends JobIntentService { ArrayList repoErrors = new ArrayList<>(); boolean changes = false; boolean singleRepoUpdate = !TextUtils.isEmpty(address); - for (final Repo repo : repos) { - if (!repo.inuse) { - continue; - } - if (singleRepoUpdate && !repo.address.equals(address)) { + for (final Repository repo : repos) { + if (!repo.getEnabled()) continue; + if (!singleRepoUpdate && repo.isSwap()) continue; + if (singleRepoUpdate && !repo.getAddress().equals(address)) { unchangedRepos++; continue; } - if (!singleRepoUpdate && repo.isSwap) { - continue; - } - - sendStatus(this, STATUS_INFO, getString(R.string.status_connecting_to_repo, repo.address)); + sendStatus(this, STATUS_INFO, getString(R.string.status_connecting_to_repo, repo.getAddress())); try { - IndexUpdater updater = new IndexV1Updater(this, repo); - if (Preferences.get().isForceOldIndexEnabled() || !updater.update()) { - updater = new IndexUpdater(getBaseContext(), repo); - updater.update(); - } - - if (updater.hasChanged()) { + 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 repoId = repo.getRepoId(); + final String certificate = Objects.requireNonNull(repo.getCertificate()); + IndexUpdateResult result = updater.update(repoId, certificate, listener); + if (result == IndexUpdateResult.UNCHANGED) { + unchangedRepos++; + } else if (result == IndexUpdateResult.PROCESSED) { updatedRepos++; changes = true; - } else { - unchangedRepos++; } - } catch (IndexUpdater.UpdateException e) { + } catch (Exception e) { errorRepos++; Throwable cause = e.getCause(); if (cause == null) { @@ -499,7 +495,7 @@ public class UpdateService extends JobIntentService { } else { repoErrors.add(e.getLocalizedMessage() + " ⇨ " + cause.getLocalizedMessage()); } - Log.e(TAG, "Error updating repository " + repo.address); + Log.e(TAG, "Error updating repository " + repo.getAddress()); e.printStackTrace(); } @@ -560,6 +556,7 @@ public class UpdateService extends JobIntentService { * to be updated, it is queued last. */ public static void autoDownloadUpdates(Context context) { + // TODO adapt to new DB List canUpdate = AppProvider.Helper.findCanUpdate(context, Schema.AppMetadataTable.Cols.ALL); String packageName = context.getPackageName(); App updateLastApp = null; @@ -588,9 +585,9 @@ public class UpdateService extends JobIntentService { } } - public static void reportDownloadProgress(Context context, IndexUpdater updater, + public static void reportDownloadProgress(Context context, String indexUrl, long bytesRead, long totalBytes) { - Utils.debugLog(TAG, "Downloading " + updater.indexUrl + "(" + bytesRead + "/" + totalBytes + ")"); + Utils.debugLog(TAG, "Downloading " + indexUrl + "(" + bytesRead + "/" + totalBytes + ")"); String downloadedSizeFriendly = Utils.getFriendlySize(bytesRead); int percent = -1; if (totalBytes > 0) { @@ -599,27 +596,26 @@ public class UpdateService extends JobIntentService { String message; if (totalBytes == -1) { message = context.getString(R.string.status_download_unknown_size, - updater.indexUrl, downloadedSizeFriendly); + indexUrl, downloadedSizeFriendly); percent = -1; } else { String totalSizeFriendly = Utils.getFriendlySize(totalBytes); message = context.getString(R.string.status_download, - updater.indexUrl, downloadedSizeFriendly, totalSizeFriendly, percent); + indexUrl, downloadedSizeFriendly, totalSizeFriendly, percent); } sendStatus(context, STATUS_INFO, message, percent); } - public static void reportProcessIndexProgress(Context context, IndexUpdater updater, - long bytesRead, long totalBytes) { - Utils.debugLog(TAG, "Processing " + updater.indexUrl + "(" + bytesRead + "/" + totalBytes + ")"); + public static void reportProcessIndexProgress(Context context, String indexUrl, long bytesRead, long totalBytes) { + Utils.debugLog(TAG, "Processing " + indexUrl + "(" + bytesRead + "/" + totalBytes + ")"); String downloadedSize = Utils.getFriendlySize(bytesRead); String totalSize = Utils.getFriendlySize(totalBytes); int percent = -1; if (totalBytes > 0) { percent = Utils.getPercent(bytesRead, totalBytes); } - String message = context.getString(R.string.status_processing_xml_percent, - updater.indexUrl, downloadedSize, totalSize, percent); + String message = context.getString(R.string.status_processing_xml_percent, indexUrl, downloadedSize, + totalSize, percent); sendStatus(context, STATUS_INFO, message, percent); } @@ -631,12 +627,11 @@ public class UpdateService extends JobIntentService { * "Saving app details" sent to the user. If you know how many apps you have * processed, then a message of "Saving app details (x/total)" is displayed. */ - public static void reportProcessingAppsProgress(Context context, IndexUpdater updater, - int appsSaved, int totalApps) { - Utils.debugLog(TAG, "Committing " + updater.indexUrl + "(" + appsSaved + "/" + totalApps + ")"); + public static void reportProcessingAppsProgress(Context context, String indexUrl, int appsSaved, int totalApps) { + Utils.debugLog(TAG, "Committing " + indexUrl + "(" + appsSaved + "/" + totalApps + ")"); if (totalApps > 0) { String message = context.getString(R.string.status_inserting_x_apps, - appsSaved, totalApps, updater.indexUrl); + appsSaved, totalApps, indexUrl); sendStatus(context, STATUS_INFO, message, Utils.getPercent(appsSaved, totalApps)); } else { String message = context.getString(R.string.status_inserting_apps); diff --git a/app/src/main/java/org/fdroid/fdroid/UpdateServiceListener.java b/app/src/main/java/org/fdroid/fdroid/UpdateServiceListener.java new file mode 100644 index 000000000..bd0959ec9 --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/UpdateServiceListener.java @@ -0,0 +1,28 @@ +package org.fdroid.fdroid; + +import android.content.Context; + +import androidx.annotation.NonNull; + +import org.fdroid.database.Repository; +import org.fdroid.fdroid.data.App; +import org.fdroid.index.IndexUpdateListener; + +class UpdateServiceListener implements IndexUpdateListener { + + private final Context context; + + UpdateServiceListener(Context context) { + this.context = context; + } + + @Override + public void onDownloadProgress(@NonNull Repository repo, long bytesRead, long totalBytes) { + UpdateService.reportDownloadProgress(context, repo.getAddress(), bytesRead, totalBytes); + } + + @Override + public void onUpdateProgress(@NonNull Repository repo, int appsProcessed, int totalApps) { + UpdateService.reportProcessingAppsProgress(context, repo.getName(App.getLocales()), appsProcessed, totalApps); + } +} diff --git a/app/src/main/java/org/fdroid/fdroid/Utils.java b/app/src/main/java/org/fdroid/fdroid/Utils.java index 8d0415e24..593db7379 100644 --- a/app/src/main/java/org/fdroid/fdroid/Utils.java +++ b/app/src/main/java/org/fdroid/fdroid/Utils.java @@ -55,11 +55,16 @@ import com.google.zxing.BarcodeFormat; import com.google.zxing.encode.Contents; import com.google.zxing.encode.QRCodeEncoder; +import org.fdroid.database.AppOverviewItem; +import org.fdroid.database.Repository; +import org.fdroid.download.DownloadRequest; +import org.fdroid.download.Mirror; import org.fdroid.fdroid.compat.FileCompat; import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.data.SanitizedFile; import org.fdroid.fdroid.data.Schema; +import org.fdroid.fdroid.net.TreeUriDownloader; import org.xml.sax.XMLReader; import java.io.Closeable; @@ -70,6 +75,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetSocketAddress; +import java.net.Proxy; import java.net.ServerSocket; import java.net.Socket; import java.nio.charset.Charset; @@ -98,10 +104,18 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.util.Consumer; +import androidx.core.util.Supplier; import androidx.core.view.DisplayCompat; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.Observer; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import info.guardianproject.netcipher.NetCipher; 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; import vendored.org.apache.commons.codec.binary.Hex; import vendored.org.apache.commons.codec.digest.DigestUtils; @@ -494,6 +508,31 @@ public final class Utils { app.loadWithGlide(context).apply(iconRequestOptions).into(iv); } + @Deprecated + public static void setIconFromRepoOrPM(@NonNull AppOverviewItem app, ImageView iv, Context context) { + String iconPath = app.getIcon(App.systemLocaleList); + if (iconPath == null) return; + if (iconRequestOptions == null) { + iconRequestOptions = new RequestOptions() + .error(R.drawable.ic_repo_app_default) + .fallback(R.drawable.ic_repo_app_default); + } + iconRequestOptions.onlyRetrieveFromCache(!Preferences.get().isBackgroundDownloadAllowed()); + + Repository repo = FDroidApp.getRepo(app.getRepoId()); + if (repo == null) return; + if (repo.getAddress().startsWith("content://")) { + // TODO check if this works + String uri = repo.getAddress() + TreeUriDownloader.ESCAPED_SLASH + iconPath; + Glide.with(context).load(uri).apply(iconRequestOptions).into(iv); + } else { + List mirrors = repo.getMirrors(); + Proxy proxy = NetCipher.getProxy(); + DownloadRequest request = new DownloadRequest(iconPath, mirrors, proxy, null, null); + Glide.with(context).load(request).apply(iconRequestOptions).into(iv); + } + } + /** * Get the checksum hash of the file {@code file} using the algorithm in {@code hashAlgo}. * {@code file} must exist on the filesystem and {@code hashAlgo} must be supported @@ -640,8 +679,20 @@ public final class Utils { return (int) TimeUnit.MILLISECONDS.toDays(msDiff); } + /** + * Calculate the number of days since the given date. + */ + public static int daysSince(long ms) { + long msDiff = Calendar.getInstance().getTimeInMillis() - ms; + return (int) TimeUnit.MILLISECONDS.toDays(msDiff); + } + public static String formatLastUpdated(@NonNull Resources res, @NonNull Date date) { - long msDiff = Calendar.getInstance().getTimeInMillis() - date.getTime(); + return formatLastUpdated(res, date.getTime()); + } + + public static String formatLastUpdated(@NonNull Resources res, long date) { + long msDiff = Calendar.getInstance().getTimeInMillis() - date; long days = msDiff / DateUtils.DAY_IN_MILLIS; long weeks = msDiff / (DateUtils.DAY_IN_MILLIS * 7); long months = msDiff / (DateUtils.DAY_IN_MILLIS * 30); @@ -918,6 +969,33 @@ public final class Utils { .doOnError(throwable -> Log.e(TAG, "Could not encode QR as bitmap", throwable)); } + public static Disposable runOffUiThread(Supplier supplier, Consumer consumer) { + return Single.fromCallable(supplier::get) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(consumer::accept); + } + + public static Disposable runOffUiThread(Runnable runnable) { + return Single.fromCallable(() -> { + runnable.run(); + return true; + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(); + } + + public static void observeOnce(LiveData liveData, LifecycleOwner lifecycleOwner, Consumer consumer) { + liveData.observe(lifecycleOwner, new Observer() { + @Override + public void onChanged(T t) { + consumer.accept(t); + liveData.removeObserver(this); + } + }); + } + /** * Keep an instance of this class as an field in an AppCompatActivity for figuring out whether the on * screen keyboard is currently visible or not. diff --git a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java index 0f3024bf5..5a223d224 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java +++ b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java @@ -355,6 +355,7 @@ public class InstalledAppProviderService extends JobIntentService { * into the database when under test. */ static void insertAppIntoDb(Context context, PackageInfo packageInfo, String hashType, String hash) { + if (true) return; Log.d(TAG, "insertAppIntoDb " + packageInfo.packageName); Uri uri = InstalledAppProvider.getContentUri(); ContentValues contentValues = new ContentValues(); diff --git a/app/src/main/java/org/fdroid/fdroid/net/DownloaderFactory.java b/app/src/main/java/org/fdroid/fdroid/net/DownloaderFactory.java index 949b59e18..22b5dff2b 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/DownloaderFactory.java +++ b/app/src/main/java/org/fdroid/fdroid/net/DownloaderFactory.java @@ -3,8 +3,10 @@ package org.fdroid.fdroid.net; import android.content.ContentResolver; import android.net.Uri; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.fdroid.database.Repository; import org.fdroid.download.DownloadRequest; import org.fdroid.download.Downloader; import org.fdroid.download.HttpDownloader; @@ -12,7 +14,6 @@ import org.fdroid.download.HttpManager; import org.fdroid.download.Mirror; import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.Utils; -import org.fdroid.fdroid.data.Repo; import java.io.File; import java.io.IOException; @@ -21,32 +22,25 @@ import java.util.List; import info.guardianproject.netcipher.NetCipher; -public class DownloaderFactory { +public class DownloaderFactory extends org.fdroid.download.DownloaderFactory { private static final String TAG = "DownloaderFactory"; // TODO move to application object or inject where needed + public static final DownloaderFactory INSTANCE = new DownloaderFactory(); public static final HttpManager HTTP_MANAGER = new HttpManager(Utils.getUserAgent(), FDroidApp.queryString, NetCipher.getProxy()); - /** - * Same as {@link #create(Repo, Uri, File)}, but trying canonical address first. - *

- * See https://gitlab.com/fdroid/fdroidclient/-/issues/1708 for why this is still needed. - */ - public static Downloader createWithTryFirstMirror(Repo repo, Uri uri, File destFile) - throws IOException { - Mirror tryFirst = new Mirror(repo.address); - List mirrors = Mirror.fromStrings(repo.getMirrorList()); - return create(repo, mirrors, uri, destFile, tryFirst); - } - - public static Downloader create(Repo repo, Uri uri, File destFile) throws IOException { - List mirrors = Mirror.fromStrings(repo.getMirrorList()); + @NonNull + @Override + public Downloader create(Repository repo, @NonNull Uri uri, @NonNull File destFile) throws IOException { + List mirrors = repo.getMirrors(); return create(repo, mirrors, uri, destFile, null); } - private static Downloader create(Repo repo, List mirrors, Uri uri, File destFile, - @Nullable Mirror tryFirst) throws IOException { + @NonNull + @Override + protected Downloader create(@NonNull Repository repo, @NonNull List mirrors, @NonNull Uri uri, + @NonNull File destFile, @Nullable Mirror tryFirst) throws IOException { Downloader downloader; String scheme = uri.getScheme(); @@ -57,11 +51,11 @@ public class DownloaderFactory { } else if (ContentResolver.SCHEME_FILE.equals(scheme)) { downloader = new LocalFileDownloader(uri, destFile); } else { - String path = uri.toString().replace(repo.address, ""); + String path = uri.toString().replace(repo.getAddress(), ""); Utils.debugLog(TAG, "Using suffix " + path + " with mirrors " + mirrors); Proxy proxy = NetCipher.getProxy(); DownloadRequest request = - new DownloadRequest(path, mirrors, proxy, repo.username, repo.password, tryFirst); + new DownloadRequest(path, mirrors, proxy, repo.getUsername(), repo.getPassword(), tryFirst); downloader = new HttpDownloader(HTTP_MANAGER, request, destFile); } return downloader; diff --git a/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java b/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java index 5eb2bdf0b..48912fb46 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java +++ b/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java @@ -33,12 +33,14 @@ import android.text.TextUtils; import android.util.Log; import android.util.LogPrinter; +import org.fdroid.database.FDroidDatabase; +import org.fdroid.database.Repository; import org.fdroid.download.Downloader; import org.fdroid.fdroid.BuildConfig; +import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.ProgressListener; import org.fdroid.fdroid.Utils; -import org.fdroid.fdroid.data.Repo; -import org.fdroid.fdroid.data.RepoProvider; +import org.fdroid.fdroid.data.DBHelper; import org.fdroid.fdroid.data.SanitizedFile; import org.fdroid.fdroid.installer.ApkCache; @@ -248,8 +250,14 @@ public class DownloaderService extends Service { try { activeCanonicalUrl = canonicalUrl.toString(); - final Repo repo = RepoProvider.Helper.findById(this, repoId); - downloader = DownloaderFactory.create(repo, canonicalUrl, localFile); + Repository repo = FDroidApp.getRepo(repoId); + if (repo == null) { + // right after the app gets re-recreated downloads get re-triggered, so repo can still be null + FDroidDatabase db = DBHelper.getDb(getApplicationContext()); + repo = db.getRepositoryDao().getRepository(repoId); + if (repo == null) return; // repo might have been deleted in the meantime + } + downloader = DownloaderFactory.INSTANCE.create(repo, canonicalUrl, localFile); downloader.setListener(new ProgressListener() { @Override public void onProgress(long bytesRead, long totalBytes) { diff --git a/app/src/main/java/org/fdroid/fdroid/receiver/PackageManagerReceiver.java b/app/src/main/java/org/fdroid/fdroid/receiver/PackageManagerReceiver.java index a91ca5780..9f6797c9a 100644 --- a/app/src/main/java/org/fdroid/fdroid/receiver/PackageManagerReceiver.java +++ b/app/src/main/java/org/fdroid/fdroid/receiver/PackageManagerReceiver.java @@ -24,6 +24,8 @@ public class PackageManagerReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { + // TODO might not be needed anymore + if (true) return; if (intent != null) { String action = intent.getAction(); if (Intent.ACTION_PACKAGE_ADDED.equals(action)) { diff --git a/app/src/main/java/org/fdroid/fdroid/views/StatusBanner.java b/app/src/main/java/org/fdroid/fdroid/views/StatusBanner.java index 9659b3bef..f3ea506ba 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/StatusBanner.java +++ b/app/src/main/java/org/fdroid/fdroid/views/StatusBanner.java @@ -11,10 +11,11 @@ import android.util.AttributeSet; import android.view.Gravity; import android.view.View; +import org.fdroid.database.Repository; +import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; import org.fdroid.fdroid.UpdateService; -import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.net.ConnectivityMonitorService; import java.util.Arrays; @@ -47,7 +48,6 @@ public class StatusBanner extends androidx.appcompat.widget.AppCompatTextView { private int networkState = ConnectivityMonitorService.FLAG_NET_NO_LIMIT; private int overDataState; private int overWiFiState; - private List localRepos; private final SharedPreferences preferences; @@ -86,7 +86,6 @@ public class StatusBanner extends androidx.appcompat.widget.AppCompatTextView { overDataState = Preferences.get().getOverData(); overWiFiState = Preferences.get().getOverWifi(); - localRepos = UpdateService.getLocalRepos(context); preferences.registerOnSharedPreferenceChangeListener(dataWifiChangeListener); setBannerTextAndVisibility(); @@ -108,8 +107,6 @@ public class StatusBanner extends androidx.appcompat.widget.AppCompatTextView { * mirror on a USB OTG thumb drive. Local repos on system partitions are * not treated as local mirrors here, they are shipped as part of the * device, and users are generally not aware of them. - * - * @see org.fdroid.fdroid.data.DBHelper#loadAdditionalRepos(String) */ private void setBannerTextAndVisibility() { if (updateServiceStatus == UpdateService.STATUS_INFO) { @@ -121,11 +118,11 @@ public class StatusBanner extends androidx.appcompat.widget.AppCompatTextView { setVisibility(View.VISIBLE); } else if (overDataState == Preferences.OVER_NETWORK_NEVER && overWiFiState == Preferences.OVER_NETWORK_NEVER) { - localRepos = UpdateService.getLocalRepos(getContext()); + List localRepos = UpdateService.getLocalRepos(FDroidApp.repos); boolean hasLocalNonSystemRepos = true; final List systemPartitions = Arrays.asList("odm", "oem", "product", "system", "vendor"); - for (Repo repo : localRepos) { - for (String segment : Uri.parse(repo.address).getPathSegments()) { + for (Repository repo : localRepos) { + for (String segment : Uri.parse(repo.getAddress()).getPathSegments()) { if (systemPartitions.contains(segment)) { hasLocalNonSystemRepos = false; } diff --git a/app/src/main/java/org/fdroid/fdroid/views/categories/AppCardController.java b/app/src/main/java/org/fdroid/fdroid/views/categories/AppCardController.java index 9232b13bb..1ff068bf9 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/categories/AppCardController.java +++ b/app/src/main/java/org/fdroid/fdroid/views/categories/AppCardController.java @@ -1,6 +1,7 @@ package org.fdroid.fdroid.views.categories; import android.content.Intent; +import android.content.res.Resources; import android.os.Bundle; import android.view.View; import android.widget.ImageView; @@ -16,10 +17,13 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityOptionsCompat; import androidx.core.content.ContextCompat; +import androidx.core.os.ConfigurationCompat; import androidx.core.util.Pair; import androidx.core.view.ViewCompat; import androidx.recyclerview.widget.RecyclerView; +import org.fdroid.database.AppOverviewItem; + /** * The {@link AppCardController} can bind an app to several different layouts, as long as the layout * contains the following elements: @@ -51,7 +55,7 @@ public class AppCardController extends RecyclerView.ViewHolder private final TextView newTag; @Nullable - private App currentApp; + private AppOverviewItem currentApp; private final AppCompatActivity activity; @@ -68,10 +72,16 @@ public class AppCardController extends RecyclerView.ViewHolder itemView.setOnClickListener(this); } - public void bindApp(@NonNull App app) { + public void bindApp(@NonNull AppOverviewItem app) { + if (App.systemLocaleList == null) { + App.systemLocaleList = ConfigurationCompat.getLocales(Resources.getSystem().getConfiguration()); + } currentApp = app; - summary.setText(Utils.formatAppNameAndSummary(app.name, app.summary)); + String name = app.getName(App.systemLocaleList); + summary.setText( + Utils.formatAppNameAndSummary(name == null ? "" : name, app.getSummary(App.systemLocaleList)) + ); if (newTag != null) { if (isConsideredNew(app)) { @@ -83,13 +93,11 @@ public class AppCardController extends RecyclerView.ViewHolder Utils.setIconFromRepoOrPM(app, icon, icon.getContext()); } - private boolean isConsideredNew(@NonNull App app) { - //noinspection SimplifiableIfStatement - if (app.added == null || app.lastUpdated == null || !app.added.equals(app.lastUpdated)) { + private boolean isConsideredNew(@NonNull AppOverviewItem app) { + if (app.getAdded() != app.getLastUpdated()) { return false; } - - return Utils.daysSince(app.added) <= DAYS_TO_CONSIDER_NEW; + return Utils.daysSince(app.getAdded()) <= DAYS_TO_CONSIDER_NEW; } /** @@ -102,7 +110,7 @@ public class AppCardController extends RecyclerView.ViewHolder } Intent intent = new Intent(activity, AppDetailsActivity.class); - intent.putExtra(AppDetailsActivity.EXTRA_APPID, currentApp.packageName); + intent.putExtra(AppDetailsActivity.EXTRA_APPID, currentApp.getPackageName()); Pair iconTransitionPair = Pair.create((View) icon, activity.getString(R.string.transition_app_item_icon)); diff --git a/app/src/main/java/org/fdroid/fdroid/views/categories/AppPreviewAdapter.java b/app/src/main/java/org/fdroid/fdroid/views/categories/AppPreviewAdapter.java index f60fed026..995d58501 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/categories/AppPreviewAdapter.java +++ b/app/src/main/java/org/fdroid/fdroid/views/categories/AppPreviewAdapter.java @@ -1,18 +1,20 @@ package org.fdroid.fdroid.views.categories; -import android.database.Cursor; import android.view.ViewGroup; +import org.fdroid.database.AppOverviewItem; import org.fdroid.fdroid.R; -import org.fdroid.fdroid.data.App; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.recyclerview.widget.RecyclerView; +import java.util.Collections; +import java.util.List; + class AppPreviewAdapter extends RecyclerView.Adapter { - private Cursor cursor; + private List items = Collections.emptyList(); private final AppCompatActivity activity; AppPreviewAdapter(AppCompatActivity activity) { @@ -28,21 +30,20 @@ class AppPreviewAdapter extends RecyclerView.Adapter { @Override public void onBindViewHolder(@NonNull AppCardController holder, int position) { - cursor.moveToPosition(position); - holder.bindApp(new App(cursor)); + holder.bindApp(items.get(position)); } @Override public int getItemCount() { - return cursor == null ? 0 : cursor.getCount(); + return items.size(); } - public void setAppCursor(Cursor cursor) { - if (this.cursor == cursor) { + void setAppCursor(List items) { + if (this.items == items) { //don't notify when the cursor did not change return; } - this.cursor = cursor; + this.items = items; notifyDataSetChanged(); } } diff --git a/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryAdapter.java b/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryAdapter.java index 41d9cf161..cc6383c3d 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryAdapter.java +++ b/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryAdapter.java @@ -2,22 +2,26 @@ package org.fdroid.fdroid.views.categories; import android.view.ViewGroup; +import org.fdroid.database.AppOverviewItem; +import org.fdroid.database.FDroidDatabase; import org.fdroid.fdroid.R; +import java.util.HashMap; import java.util.List; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; -import androidx.loader.app.LoaderManager; +import androidx.lifecycle.LiveData; import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.ListAdapter; public class CategoryAdapter extends ListAdapter { private final AppCompatActivity activity; - private final LoaderManager loaderManager; + private final FDroidDatabase db; + private final HashMap>> liveData = new HashMap<>(); - public CategoryAdapter(AppCompatActivity activity, LoaderManager loaderManager) { + public CategoryAdapter(AppCompatActivity activity, FDroidDatabase db) { super(new DiffUtil.ItemCallback() { @Override public boolean areItemsTheSame(String oldItem, String newItem) { @@ -31,23 +35,30 @@ public class CategoryAdapter extends ListAdapter { }); this.activity = activity; - this.loaderManager = loaderManager; + this.db = db; } @NonNull @Override public CategoryController onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - return new CategoryController(activity, loaderManager, activity.getLayoutInflater() + return new CategoryController(activity, activity.getLayoutInflater() .inflate(R.layout.category_item, parent, false)); } @Override public void onBindViewHolder(@NonNull CategoryController holder, int position) { - holder.bindModel(getItem(position)); + String categoryName = getItem(position); + holder.bindModel(categoryName, liveData.get(categoryName)); } public void setCategories(@NonNull List unlocalizedCategoryNames) { submitList(unlocalizedCategoryNames); + for (String name: unlocalizedCategoryNames) { + int num = CategoryController.NUM_OF_APPS_PER_CATEGORY_ON_OVERVIEW; + // we are getting the LiveData here and not in the ViewHolder, so the data gets cached here + // this prevents reloads when scrolling + liveData.put(name, db.getAppDao().getAppOverviewItems(name, num)); + } } } diff --git a/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryController.java b/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryController.java index 390d8de02..f1b9a283f 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryController.java +++ b/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryController.java @@ -3,10 +3,8 @@ package org.fdroid.fdroid.views.categories; import android.content.Context; import android.content.Intent; import android.content.res.Resources; -import android.database.Cursor; import android.graphics.Color; import android.graphics.Rect; -import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.FrameLayout; @@ -14,28 +12,33 @@ import android.widget.TextView; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; import androidx.core.view.ViewCompat; -import androidx.loader.app.LoaderManager; -import androidx.loader.content.CursorLoader; -import androidx.loader.content.Loader; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.Observer; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; +import org.fdroid.database.AppOverviewItem; +import org.fdroid.database.FDroidDatabase; +import org.fdroid.database.FDroidDatabaseHolder; import org.fdroid.fdroid.R; -import org.fdroid.fdroid.Utils; -import org.fdroid.fdroid.data.AppProvider; -import org.fdroid.fdroid.data.Schema; -import org.fdroid.fdroid.data.Schema.AppMetadataTable.Cols; import org.fdroid.fdroid.views.apps.AppListActivity; import org.fdroid.fdroid.views.apps.FeatureImage; +import java.util.List; import java.util.Locale; import java.util.Random; -public class CategoryController extends RecyclerView.ViewHolder implements LoaderManager.LoaderCallbacks { +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; + +public class CategoryController extends RecyclerView.ViewHolder { private final Button viewAll; private final TextView heading; private final FeatureImage image; @@ -43,16 +46,18 @@ public class CategoryController extends RecyclerView.ViewHolder implements Loade private final FrameLayout background; private final AppCompatActivity activity; - private final LoaderManager loaderManager; - private static final int NUM_OF_APPS_PER_CATEGORY_ON_OVERVIEW = 20; + private final FDroidDatabase db; + static final int NUM_OF_APPS_PER_CATEGORY_ON_OVERVIEW = 20; private String currentCategory; + @Nullable + private Disposable disposable; - CategoryController(final AppCompatActivity activity, LoaderManager loaderManager, View itemView) { + CategoryController(final AppCompatActivity activity, View itemView) { super(itemView); this.activity = activity; - this.loaderManager = loaderManager; + db = FDroidDatabaseHolder.getDb(activity); appCardsAdapter = new AppPreviewAdapter(activity); @@ -73,7 +78,8 @@ public class CategoryController extends RecyclerView.ViewHolder implements Loade return categoryNameId == 0 ? categoryName : context.getString(categoryNameId); } - void bindModel(@NonNull String categoryName) { + void bindModel(@NonNull String categoryName, LiveData> liveData) { + loadAppItems(liveData); currentCategory = categoryName; String translatedName = translateCategory(activity, categoryName); @@ -81,9 +87,7 @@ public class CategoryController extends RecyclerView.ViewHolder implements Loade heading.setContentDescription(activity.getString(R.string.tts_category_name, translatedName)); viewAll.setVisibility(View.INVISIBLE); - - loaderManager.initLoader(currentCategory.hashCode(), null, this); - loaderManager.initLoader(currentCategory.hashCode() + 1, null, this); + loadNumAppsInCategory(); @ColorInt int backgroundColour = getBackgroundColour(activity, categoryName); background.setBackgroundColor(backgroundColour); @@ -98,6 +102,26 @@ public class CategoryController extends RecyclerView.ViewHolder implements Loade } } + private void loadAppItems(LiveData> liveData) { + setIsRecyclable(false); + liveData.observe(activity, new Observer>() { + @Override + public void onChanged(List items) { + appCardsAdapter.setAppCursor(items); + setIsRecyclable(true); + liveData.removeObserver(this); + } + }); + } + + private void loadNumAppsInCategory() { + if (disposable != null) disposable.dispose(); + disposable = Single.fromCallable(() -> db.getAppDao().getNumberOfAppsInCategory(currentCategory)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::setNumAppsInCategory); + } + /** * @param requiresLowerCaseId Previously categories were translated using strings such as "category_Reading" * for the "Reading" category. Now we also need to have drawable resources such as @@ -130,94 +154,13 @@ public class CategoryController extends RecyclerView.ViewHolder implements Loade return Color.HSVToColor(hsv); } - /** - * Return either the total apps in the category, or the entries to display - * for a category, depending on the value of {@code id}. This uses a sort - * similar to the one in {@link org.fdroid.fdroid.views.main.LatestViewBinder#onCreateLoader(int, Bundle)}. - * The difference is that this does not treat "new" app any differently. - * - * @see AppProvider#getCategoryUri(String) - * @see AppProvider#getTopFromCategoryUri(String, int) - * @see AppProvider#query(android.net.Uri, String[], String, String[], String) - * @see AppProvider#TOP_FROM_CATEGORY - * @see org.fdroid.fdroid.views.main.LatestViewBinder#onCreateLoader(int, Bundle) - */ - @NonNull - @Override - public Loader onCreateLoader(int id, Bundle args) { - final String table = Schema.AppMetadataTable.NAME; - final String added = table + "." + Cols.ADDED; - final String lastUpdated = table + "." + Cols.LAST_UPDATED; - if (id == currentCategory.hashCode() + 1) { - return new CursorLoader( - activity, - AppProvider.getCategoryUri(currentCategory), - new String[]{Schema.AppMetadataTable.Cols._COUNT}, - Utils.getAntifeatureSQLFilter(activity), - null, - null - ); - } else { - return new CursorLoader( - activity, - AppProvider.getTopFromCategoryUri(currentCategory, NUM_OF_APPS_PER_CATEGORY_ON_OVERVIEW), - new String[]{ - Schema.AppMetadataTable.Cols.NAME, - Schema.AppMetadataTable.Cols.Package.PACKAGE_NAME, - Schema.AppMetadataTable.Cols.SUMMARY, - Schema.AppMetadataTable.Cols.ICON_URL, - Schema.AppMetadataTable.Cols.ICON, - Schema.AppMetadataTable.Cols.REPO_ID, - }, - Utils.getAntifeatureSQLFilter(activity), - null, - table + "." + Cols.IS_LOCALIZED + " DESC" - + ", " + table + "." + Cols.NAME + " IS NULL ASC" - + ", CASE WHEN " + table + "." + Cols.ICON + " IS NULL" - + " AND " + table + "." + Cols.ICON_URL + " IS NULL" - + " THEN 1 ELSE 0 END" - + ", " + table + "." + Cols.SUMMARY + " IS NULL ASC" - + ", " + table + "." + Cols.DESCRIPTION + " IS NULL ASC" - + ", CASE WHEN " + table + "." + Cols.PHONE_SCREENSHOTS + " IS NULL" - + " AND " + table + "." + Cols.SEVEN_INCH_SCREENSHOTS + " IS NULL" - + " AND " + table + "." + Cols.TEN_INCH_SCREENSHOTS + " IS NULL" - + " AND " + table + "." + Cols.TV_SCREENSHOTS + " IS NULL" - + " AND " + table + "." + Cols.WEAR_SCREENSHOTS + " IS NULL" - + " AND " + table + "." + Cols.FEATURE_GRAPHIC + " IS NULL" - + " AND " + table + "." + Cols.PROMO_GRAPHIC + " IS NULL" - + " AND " + table + "." + Cols.TV_BANNER + " IS NULL" - + " THEN 1 ELSE 0 END" - + ", " + lastUpdated + " DESC" - + ", " + added + " ASC" - ); - } - } - - @Override - public void onLoadFinished(@NonNull Loader loader, Cursor cursor) { - int topAppsId = currentCategory.hashCode(); - int countAllAppsId = topAppsId + 1; - - // Anything other than these IDs indicates that the loader which just finished - // is no longer the one this view holder is interested in, due to the user having - // scrolled away already during the asynchronous query being run. - if (loader.getId() == topAppsId) { - appCardsAdapter.setAppCursor(cursor); - } else if (loader.getId() == countAllAppsId) { - cursor.moveToFirst(); - int numAppsInCategory = cursor.getInt(0); - viewAll.setVisibility(View.VISIBLE); - Resources r = activity.getResources(); - viewAll.setText(r.getQuantityString(R.plurals.button_view_all_apps_in_category, numAppsInCategory, - numAppsInCategory)); - viewAll.setContentDescription(r.getQuantityString(R.plurals.tts_view_all_in_category, numAppsInCategory, - numAppsInCategory, currentCategory)); - } - } - - @Override - public void onLoaderReset(@NonNull Loader loader) { - appCardsAdapter.setAppCursor(null); + private void setNumAppsInCategory(int numAppsInCategory) { + viewAll.setVisibility(View.VISIBLE); + Resources r = activity.getResources(); + viewAll.setText(r.getQuantityString(R.plurals.button_view_all_apps_in_category, numAppsInCategory, + numAppsInCategory)); + viewAll.setContentDescription(r.getQuantityString(R.plurals.tts_view_all_in_category, numAppsInCategory, + numAppsInCategory, currentCategory)); } @SuppressWarnings("FieldCanBeLocal") diff --git a/app/src/main/java/org/fdroid/fdroid/views/main/LatestAdapter.java b/app/src/main/java/org/fdroid/fdroid/views/main/LatestAdapter.java index 8d3557ab0..b3407ccac 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/main/LatestAdapter.java +++ b/app/src/main/java/org/fdroid/fdroid/views/main/LatestAdapter.java @@ -2,25 +2,28 @@ package org.fdroid.fdroid.views.main; import android.content.Context; import android.content.res.Resources; -import android.database.Cursor; import android.graphics.Rect; import android.view.View; import android.view.ViewGroup; import org.fdroid.fdroid.BuildConfig; import org.fdroid.fdroid.R; -import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.views.categories.AppCardController; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.core.view.ViewCompat; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import org.fdroid.database.AppOverviewItem; + +import java.util.List; + public class LatestAdapter extends RecyclerView.Adapter { - private Cursor cursor; + private List apps; private final AppCompatActivity activity; private final RecyclerView.ItemDecoration appListDecorator; @@ -95,22 +98,21 @@ public class LatestAdapter extends RecyclerView.Adapter { @Override public void onBindViewHolder(@NonNull AppCardController holder, int position) { - cursor.moveToPosition(position); - final App app = new App(cursor); + final AppOverviewItem app = apps.get(position); holder.bindApp(app); } @Override public int getItemCount() { - return cursor == null ? 0 : cursor.getCount(); + return apps == null ? 0 : apps.size(); } - public void setAppsCursor(Cursor cursor) { - if (this.cursor == cursor) { - //don't notify when the cursor did not change + public void setApps(@Nullable List apps) { + if (this.apps == apps) { + //don't notify when the apps did not change return; } - this.cursor = cursor; + this.apps = apps; notifyDataSetChanged(); } diff --git a/app/src/main/java/org/fdroid/fdroid/views/main/LatestViewBinder.java b/app/src/main/java/org/fdroid/fdroid/views/main/LatestViewBinder.java index 7e27c2817..69bb763b3 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/main/LatestViewBinder.java +++ b/app/src/main/java/org/fdroid/fdroid/views/main/LatestViewBinder.java @@ -1,8 +1,6 @@ package org.fdroid.fdroid.views.main; import android.content.Intent; -import android.database.Cursor; -import android.os.Bundle; import android.view.View; import android.widget.FrameLayout; import android.widget.LinearLayout; @@ -11,26 +9,31 @@ import android.widget.TextView; import com.google.android.material.floatingactionbutton.FloatingActionButton; +import org.fdroid.database.AppOverviewItem; +import org.fdroid.database.FDroidDatabase; +import org.fdroid.database.Repository; +import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.Preferences; +import org.fdroid.fdroid.Preferences.ChangeListener; import org.fdroid.fdroid.R; import org.fdroid.fdroid.UpdateService; import org.fdroid.fdroid.Utils; -import org.fdroid.fdroid.data.AppProvider; -import org.fdroid.fdroid.data.RepoProvider; -import org.fdroid.fdroid.data.Schema.AppMetadataTable; -import org.fdroid.fdroid.data.Schema.AppMetadataTable.Cols; -import org.fdroid.fdroid.data.Schema.RepoTable; +import org.fdroid.fdroid.data.DBHelper; import org.fdroid.fdroid.panic.HidingManager; import org.fdroid.fdroid.views.apps.AppListActivity; -import org.fdroid.fdroid.views.categories.AppCardController; -import java.util.Date; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Set; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; -import androidx.loader.app.LoaderManager; -import androidx.loader.content.CursorLoader; -import androidx.loader.content.Loader; +import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.Observer; +import androidx.lifecycle.Transformations; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; @@ -38,19 +41,31 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; /** * Loads a list of newly added or recently updated apps and displays them to the user. */ -class LatestViewBinder implements LoaderManager.LoaderCallbacks { - - private static final int LOADER_ID = 978015789; +class LatestViewBinder implements Observer>, ChangeListener { private final LatestAdapter latestAdapter; private final AppCompatActivity activity; private final TextView emptyState; private final RecyclerView appList; + private final FDroidDatabase db; private ProgressBar progressBar; LatestViewBinder(final AppCompatActivity activity, FrameLayout parent) { this.activity = activity; + activity.getLifecycle().addObserver(new DefaultLifecycleObserver() { + @Override + public void onCreate(@NonNull LifecycleOwner owner) { + Preferences.get().registerAppsRequiringAntiFeaturesChangeListener(LatestViewBinder.this); + } + + @Override + public void onDestroy(@NonNull LifecycleOwner owner) { + Preferences.get().unregisterAppsRequiringAntiFeaturesChangeListener(LatestViewBinder.this); + } + }); + db = DBHelper.getDb(activity); + Transformations.distinctUntilChanged(db.getAppDao().getAppOverviewItems(200)).observe(activity, this); View latestView = activity.getLayoutInflater().inflate(R.layout.main_tab_latest, parent, true); @@ -95,69 +110,13 @@ class LatestViewBinder implements LoaderManager.LoaderCallbacks { } } }); - - activity.getSupportLoaderManager().initLoader(LOADER_ID, null, this); - } - - /** - * Sort by localized first so users see entries in their language, - * then sort by highlighted fields, then sort by whether the app is new, - * then if it has WhatsNew/Changelog entries, then by when it was last - * updated. Last, it sorts by the date the app was added, putting older - * ones first, to give preference to apps that have been maintained in - * F-Droid longer. - * - * @see AppProvider#getLatestTabUri() - */ - @NonNull - @Override - public Loader onCreateLoader(int id, Bundle args) { - if (id != LOADER_ID) { - return null; - } - final String table = AppMetadataTable.NAME; - final String added = table + "." + Cols.ADDED; - final String lastUpdated = table + "." + Cols.LAST_UPDATED; - return new CursorLoader( - activity, - AppProvider.getLatestTabUri(), - AppMetadataTable.Cols.ALL, - Utils.getAntifeatureSQLFilter(activity), - null, - table + "." + Cols.IS_LOCALIZED + " DESC" - + ", " + table + "." + Cols.NAME + " IS NULL ASC" - + ", CASE WHEN " + table + "." + Cols.ICON + " IS NULL" - + " AND " + table + "." + Cols.ICON_URL + " IS NULL" - + " THEN 1 ELSE 0 END" - + ", " + table + "." + Cols.SUMMARY + " IS NULL ASC" - + ", " + table + "." + Cols.DESCRIPTION + " IS NULL ASC" - + ", CASE WHEN " + table + "." + Cols.PHONE_SCREENSHOTS + " IS NULL" - + " AND " + table + "." + Cols.SEVEN_INCH_SCREENSHOTS + " IS NULL" - + " AND " + table + "." + Cols.TEN_INCH_SCREENSHOTS + " IS NULL" - + " AND " + table + "." + Cols.TV_SCREENSHOTS + " IS NULL" - + " AND " + table + "." + Cols.WEAR_SCREENSHOTS + " IS NULL" - + " AND " + table + "." + Cols.FEATURE_GRAPHIC + " IS NULL" - + " AND " + table + "." + Cols.PROMO_GRAPHIC + " IS NULL" - + " AND " + table + "." + Cols.TV_BANNER + " IS NULL" - + " THEN 1 ELSE 0 END" - + ", CASE WHEN date(" + added + ") >= date(" + lastUpdated + ")" - + " AND date((SELECT " + RepoTable.Cols.LAST_UPDATED + " FROM " + RepoTable.NAME - + " WHERE _id=" + table + "." + Cols.REPO_ID - + " ),'-" + AppCardController.DAYS_TO_CONSIDER_NEW + " days') " - + " < date(" + lastUpdated + ")" - + " THEN 0 ELSE 1 END" - + ", " + table + "." + Cols.WHATSNEW + " IS NULL ASC" - + ", " + lastUpdated + " DESC" - + ", " + added + " ASC"); } @Override - public void onLoadFinished(@NonNull Loader loader, Cursor cursor) { - if (loader.getId() != LOADER_ID) { - return; - } - - latestAdapter.setAppsCursor(cursor); + public void onChanged(List items) { + // filter out anti-features first + filterApps(items); + latestAdapter.setApps(items); if (latestAdapter.getItemCount() == 0) { emptyState.setVisibility(View.VISIBLE); @@ -169,6 +128,48 @@ class LatestViewBinder implements LoaderManager.LoaderCallbacks { } } + @Override + public void onPreferenceChange() { + // reload and re-filter apps from DB when anti-feature settings change + LiveData> liveData = db.getAppDao().getAppOverviewItems(200); + liveData.observe(activity, new Observer>() { + @Override + public void onChanged(List items) { + LatestViewBinder.this.onChanged(items); + liveData.removeObserver(this); + } + }); + } + + private void filterApps(List items) { + List antiFeatures = Arrays.asList(activity.getResources().getStringArray(R.array.antifeaturesValues)); + Set shownAntiFeatures = Preferences.get().showAppsWithAntiFeatures(); + String otherAntiFeatures = activity.getResources().getString(R.string.antiothers_key); + boolean showOtherAntiFeatures = shownAntiFeatures.contains(otherAntiFeatures); + Iterator iterator = items.iterator(); + while (iterator.hasNext()) { + AppOverviewItem item = iterator.next(); + if (isFilteredByAntiFeature(item, antiFeatures, shownAntiFeatures, showOtherAntiFeatures)) { + iterator.remove(); + } + } + } + + private boolean isFilteredByAntiFeature(AppOverviewItem item, List antiFeatures, + Set showAntiFeatures, boolean showOther) { + for (String antiFeature : item.getAntiFeatureKeys()) { + // is it part of the known anti-features? + if (antiFeatures.contains(antiFeature)) { + // it gets filtered not part of the ones that we show + if (!showAntiFeatures.contains(antiFeature)) return true; + } else if (!showOther) { + // gets filtered if we should no show unknown anti-features + return true; + } + } + return false; + } + private void explainEmptyStateToUser() { if (Preferences.get().isIndexNeverUpdated() && UpdateService.isUpdating()) { if (progressBar != null) { @@ -187,11 +188,20 @@ class LatestViewBinder implements LoaderManager.LoaderCallbacks { emptyStateText.append(activity.getString(R.string.latest__empty_state__no_recent_apps)); emptyStateText.append("\n\n"); - int repoCount = RepoProvider.Helper.countEnabledRepos(activity); + int repoCount = 0; + Long lastUpdate = null; + for (Repository repo : FDroidApp.repos) { + if (repo.getEnabled()) { + repoCount++; + if (lastUpdate == null && repo.getLastUpdated() != null) lastUpdate = repo.getLastUpdated(); + else if (lastUpdate != null && repo.getLastUpdated() != null && repo.getLastUpdated() > lastUpdate) { + lastUpdate = repo.getLastUpdated(); + } + } + } if (repoCount == 0) { emptyStateText.append(activity.getString(R.string.latest__empty_state__no_enabled_repos)); } else { - Date lastUpdate = RepoProvider.Helper.lastUpdate(activity); if (lastUpdate == null) { emptyStateText.append(activity.getString(R.string.latest__empty_state__never_updated)); } else { @@ -201,13 +211,4 @@ class LatestViewBinder implements LoaderManager.LoaderCallbacks { emptyState.setText(emptyStateText.toString()); } - - @Override - public void onLoaderReset(@NonNull Loader loader) { - if (loader.getId() != LOADER_ID) { - return; - } - - latestAdapter.setAppsCursor(null); - } }