mirror of
https://github.com/f-droid/fdroidclient.git
synced 2026-04-20 06:47:06 -04:00
[app] Use new IndexV1Updater and make latest and category tab use new DB
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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<Cursor> {
|
||||
class CategoriesViewBinder implements Observer<List<Category>> {
|
||||
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<Cursor> {
|
||||
|
||||
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<Cursor> {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
activity.getSupportLoaderManager().restartLoader(LOADER_ID, null, this);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Cursor> 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}.
|
||||
* <p>
|
||||
* 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<Cursor> loader, Cursor cursor) {
|
||||
if (loader.getId() != LOADER_ID || cursor == null) {
|
||||
return;
|
||||
public void onChanged(List<Category> categories) {
|
||||
List<String> categoryNames = new ArrayList<>(categories.size());
|
||||
for (Category c : categories) {
|
||||
categoryNames.add(c.getId());
|
||||
}
|
||||
|
||||
List<String> 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<String>() {
|
||||
@Override
|
||||
public int compare(String categoryOne, String categoryTwo) {
|
||||
@@ -155,13 +120,4 @@ class CategoriesViewBinder implements LoaderManager.LoaderCallbacks<Cursor> {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<Cursor> loader) {
|
||||
if (loader.getId() != LOADER_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
categoryAdapter.setCategories(Collections.<String>emptyList());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<Repository> 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,
|
||||
@@ -528,6 +553,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;
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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}.
|
||||
|
||||
@@ -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<Repo> 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<Repo> getLocalRepos(List<Repo> repos) {
|
||||
ArrayList<Repo> localRepos = new ArrayList<>();
|
||||
for (Repo repo : repos) {
|
||||
if (isLocalRepoAddress(repo.address)) {
|
||||
public static List<Repository> getLocalRepos(List<Repository> repos) {
|
||||
ArrayList<Repository> 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<Repo> 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<Repository> 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<Repo> localRepos = getLocalRepos(repos);
|
||||
List<Repository> 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<CharSequence> 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<App> 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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -101,7 +107,12 @@ 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;
|
||||
@@ -497,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<Mirror> 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
|
||||
@@ -642,8 +678,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);
|
||||
@@ -920,6 +968,33 @@ public final class Utils {
|
||||
.doOnError(throwable -> Log.e(TAG, "Could not encode QR as bitmap", throwable));
|
||||
}
|
||||
|
||||
public static <T> Disposable runOffUiThread(Supplier<T> supplier, Consumer<T> 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 <T> void observeOnce(LiveData<T> liveData, LifecycleOwner lifecycleOwner, Consumer<T> consumer) {
|
||||
liveData.observe(lifecycleOwner, new Observer<T>() {
|
||||
@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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
* <p>
|
||||
* 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<Mirror> 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<Mirror> mirrors = Mirror.fromStrings(repo.getMirrorList());
|
||||
@NonNull
|
||||
@Override
|
||||
public Downloader create(Repository repo, @NonNull Uri uri, @NonNull File destFile) throws IOException {
|
||||
List<Mirror> mirrors = repo.getMirrors();
|
||||
return create(repo, mirrors, uri, destFile, null);
|
||||
}
|
||||
|
||||
private static Downloader create(Repo repo, List<Mirror> mirrors, Uri uri, File destFile,
|
||||
@Nullable Mirror tryFirst) throws IOException {
|
||||
@NonNull
|
||||
@Override
|
||||
protected Downloader create(@NonNull Repository repo, @NonNull List<Mirror> 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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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<Repo> 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<Repository> localRepos = UpdateService.getLocalRepos(FDroidApp.repos);
|
||||
boolean hasLocalNonSystemRepos = true;
|
||||
final List<String> 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;
|
||||
}
|
||||
|
||||
@@ -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<View, String> iconTransitionPair = Pair.create((View) icon,
|
||||
activity.getString(R.string.transition_app_item_icon));
|
||||
|
||||
|
||||
@@ -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<AppCardController> {
|
||||
|
||||
private Cursor cursor;
|
||||
private List<AppOverviewItem> items = Collections.emptyList();
|
||||
private final AppCompatActivity activity;
|
||||
|
||||
AppPreviewAdapter(AppCompatActivity activity) {
|
||||
@@ -28,21 +30,20 @@ class AppPreviewAdapter extends RecyclerView.Adapter<AppCardController> {
|
||||
|
||||
@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<AppOverviewItem> items) {
|
||||
if (this.items == items) {
|
||||
//don't notify when the cursor did not change
|
||||
return;
|
||||
}
|
||||
this.cursor = cursor;
|
||||
this.items = items;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String, CategoryController> {
|
||||
|
||||
private final AppCompatActivity activity;
|
||||
private final LoaderManager loaderManager;
|
||||
private final FDroidDatabase db;
|
||||
private final HashMap<String, LiveData<List<AppOverviewItem>>> liveData = new HashMap<>();
|
||||
|
||||
public CategoryAdapter(AppCompatActivity activity, LoaderManager loaderManager) {
|
||||
public CategoryAdapter(AppCompatActivity activity, FDroidDatabase db) {
|
||||
super(new DiffUtil.ItemCallback<String>() {
|
||||
@Override
|
||||
public boolean areItemsTheSame(String oldItem, String newItem) {
|
||||
@@ -31,23 +35,30 @@ public class CategoryAdapter extends ListAdapter<String, CategoryController> {
|
||||
});
|
||||
|
||||
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<String> 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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<Cursor> {
|
||||
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<List<AppOverviewItem>> 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<List<AppOverviewItem>> liveData) {
|
||||
setIsRecyclable(false);
|
||||
liveData.observe(activity, new Observer<List<AppOverviewItem>>() {
|
||||
@Override
|
||||
public void onChanged(List<AppOverviewItem> 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<Cursor> 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<Cursor> 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<Cursor> 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")
|
||||
|
||||
@@ -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<AppCardController> {
|
||||
|
||||
private Cursor cursor;
|
||||
private List<AppOverviewItem> apps;
|
||||
private final AppCompatActivity activity;
|
||||
private final RecyclerView.ItemDecoration appListDecorator;
|
||||
|
||||
@@ -95,22 +98,21 @@ public class LatestAdapter extends RecyclerView.Adapter<AppCardController> {
|
||||
|
||||
@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<AppOverviewItem> apps) {
|
||||
if (this.apps == apps) {
|
||||
//don't notify when the apps did not change
|
||||
return;
|
||||
}
|
||||
this.cursor = cursor;
|
||||
this.apps = apps;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Cursor> {
|
||||
|
||||
private static final int LOADER_ID = 978015789;
|
||||
class LatestViewBinder implements Observer<List<AppOverviewItem>>, 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<Cursor> {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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<Cursor> 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<Cursor> loader, Cursor cursor) {
|
||||
if (loader.getId() != LOADER_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
latestAdapter.setAppsCursor(cursor);
|
||||
public void onChanged(List<AppOverviewItem> 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<Cursor> {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreferenceChange() {
|
||||
// reload and re-filter apps from DB when anti-feature settings change
|
||||
LiveData<List<AppOverviewItem>> liveData = db.getAppDao().getAppOverviewItems(200);
|
||||
liveData.observe(activity, new Observer<List<AppOverviewItem>>() {
|
||||
@Override
|
||||
public void onChanged(List<AppOverviewItem> items) {
|
||||
LatestViewBinder.this.onChanged(items);
|
||||
liveData.removeObserver(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void filterApps(List<AppOverviewItem> items) {
|
||||
List<String> antiFeatures = Arrays.asList(activity.getResources().getStringArray(R.array.antifeaturesValues));
|
||||
Set<String> shownAntiFeatures = Preferences.get().showAppsWithAntiFeatures();
|
||||
String otherAntiFeatures = activity.getResources().getString(R.string.antiothers_key);
|
||||
boolean showOtherAntiFeatures = shownAntiFeatures.contains(otherAntiFeatures);
|
||||
Iterator<AppOverviewItem> iterator = items.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
AppOverviewItem item = iterator.next();
|
||||
if (isFilteredByAntiFeature(item, antiFeatures, shownAntiFeatures, showOtherAntiFeatures)) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isFilteredByAntiFeature(AppOverviewItem item, List<String> antiFeatures,
|
||||
Set<String> 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<Cursor> {
|
||||
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<Cursor> {
|
||||
|
||||
emptyState.setText(emptyStateText.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
||||
if (loader.getId() != LOADER_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
latestAdapter.setAppsCursor(null);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user