[app] Move managing repos to new DB

This commit is contained in:
Torsten Grote
2022-03-24 17:42:44 -03:00
committed by Hans-Christoph Steiner
parent 0a7debeb30
commit adaf2a97ef
16 changed files with 395 additions and 331 deletions

View File

@@ -32,13 +32,13 @@ import android.widget.Toast;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.fdroid.database.Repository;
import org.fdroid.fdroid.AddRepoIntentService;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.IndexUpdater;
import org.fdroid.fdroid.IndexV1Updater;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider;
import java.io.File;
import java.io.IOException;
@@ -196,11 +196,11 @@ public class TreeUriScannerIntentService extends IntentService {
destFile.delete();
Log.i(TAG, "Found a valid, signed index-v1.json");
for (Repo repo : RepoProvider.Helper.all(context)) {
if (fingerprint.equals(repo.fingerprint)) {
Log.i(TAG, repo.address + " has the SAME fingerprint: " + fingerprint);
for (Repository repo : FDroidApp.repos) {
if (fingerprint.equals(repo.getFingerprint())) {
Log.i(TAG, repo.getAddress() + " has the SAME fingerprint: " + fingerprint);
} else {
Log.i(TAG, repo.address + " different fingerprint");
Log.i(TAG, repo.getAddress() + " different fingerprint");
}
}

View File

@@ -14,6 +14,7 @@ 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.DBHelper;
import org.fdroid.fdroid.panic.HidingManager;
import org.fdroid.fdroid.views.apps.AppListActivity;
import org.fdroid.fdroid.views.categories.CategoryAdapter;
@@ -46,7 +47,7 @@ class CategoriesViewBinder implements Observer<List<Category>> {
CategoriesViewBinder(final AppCompatActivity activity, FrameLayout parent) {
this.activity = activity;
FDroidDatabase db = FDroidDatabaseHolder.getDb(activity);
FDroidDatabase db = DBHelper.getDb(activity);
Transformations.distinctUntilChanged(db.getRepositoryDao().getLiveCategories()).observe(activity, this);
View categoriesView = activity.getLayoutInflater().inflate(R.layout.main_tab_categories, parent, true);

View File

@@ -8,8 +8,8 @@ import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.database.Repository;
import org.fdroid.download.Mirror;
import org.fdroid.fdroid.views.ManageReposActivity;
import org.fdroid.fdroid.views.main.MainActivity;
@@ -76,14 +76,14 @@ public class AddRepoIntentService extends IntentService {
}
String fingerprint = uri.getQueryParameter("fingerprint");
for (Repo repo : RepoProvider.Helper.all(this)) {
if (repo.inuse && TextUtils.equals(fingerprint, repo.fingerprint)) {
if (TextUtils.equals(urlString, repo.address)) {
for (Repository repo : FDroidApp.repos) {
if (repo.getEnabled() && TextUtils.equals(fingerprint, repo.getFingerprint())) {
if (TextUtils.equals(urlString, repo.getAddress())) {
Utils.debugLog(TAG, urlString + " already added as a repo");
return;
} else {
for (String mirrorUrl : repo.getMirrorList()) {
if (urlString.startsWith(mirrorUrl)) {
for (Mirror mirror : repo.getMirrors()) {
if (urlString.startsWith(mirror.getBaseUrl())) {
Utils.debugLog(TAG, urlString + " already added as a mirror");
return;
}

View File

@@ -11,7 +11,6 @@ import android.os.Parcelable;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.installer.ErrorDialogActivity;
import org.fdroid.fdroid.installer.InstallManagerService;
import org.fdroid.fdroid.net.DownloaderService;
@@ -208,12 +207,12 @@ public final class AppUpdateStatusManager {
localBroadcastManager = LocalBroadcastManager.getInstance(context.getApplicationContext());
}
public void removeAllByRepo(Repo repo) {
public void removeAllByRepo(long repoId) {
boolean hasRemovedSome = false;
Iterator<AppUpdateStatus> it = getAll().iterator();
while (it.hasNext()) {
AppUpdateStatus status = it.next();
if (status.apk.repoId == repo.getId()) {
if (status.apk.repoId == repoId) {
it.remove();
hasRemovedSome = true;
}

View File

@@ -53,7 +53,6 @@ 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;
@@ -337,7 +336,7 @@ 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);
FDroidDatabase db = DBHelper.getDb(this);
db.getRepositoryDao().getLiveRepositories().observeForever(repositories -> repos = repositories);
PRNGFixes.apply();

View File

@@ -57,10 +57,10 @@ 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;
import androidx.annotation.Nullable;
import androidx.core.app.JobIntentService;
import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat;
@@ -77,6 +77,8 @@ public class UpdateService extends JobIntentService {
public static final String LOCAL_ACTION_STATUS = "status";
public static final String EXTRA_MESSAGE = "msg";
public static final String EXTRA_REPO_ID = "repoId";
public static final String EXTRA_REPO_FINGERPRINT = "fingerprint";
public static final String EXTRA_REPO_ERRORS = "repoErrors";
public static final String EXTRA_STATUS_CODE = "status";
public static final String EXTRA_MANUAL_UPDATE = "manualUpdate";
@@ -112,8 +114,13 @@ public class UpdateService extends JobIntentService {
}
public static void updateRepoNow(Context context, String address) {
updateNewRepoNow(context, address, null);
}
public static void updateNewRepoNow(Context context, String address, @Nullable String fingerprint) {
Intent intent = new Intent(context, UpdateService.class);
intent.putExtra(EXTRA_MANUAL_UPDATE, true);
intent.putExtra(EXTRA_REPO_FINGERPRINT, fingerprint);
if (!TextUtils.isEmpty(address)) {
intent.setData(Uri.parse(address));
}
@@ -416,6 +423,8 @@ public class UpdateService extends JobIntentService {
final long startTime = System.currentTimeMillis();
boolean manualUpdate = intent.getBooleanExtra(EXTRA_MANUAL_UPDATE, false);
boolean forcedUpdate = intent.getBooleanExtra(EXTRA_FORCED_UPDATE, false);
long repoId = intent.getLongExtra(EXTRA_REPO_ID, -1);
String fingerprint = intent.getStringExtra(EXTRA_REPO_FINGERPRINT);
String address = intent.getDataString();
try {
@@ -461,11 +470,10 @@ public class UpdateService extends JobIntentService {
int errorRepos = 0;
ArrayList<CharSequence> repoErrors = new ArrayList<>();
boolean changes = false;
boolean singleRepoUpdate = !TextUtils.isEmpty(address);
boolean singleRepoUpdate = !TextUtils.isEmpty(address) || repoId > 0;
for (final Repository repo : repos) {
if (!repo.getEnabled()) continue;
if (!singleRepoUpdate && repo.isSwap()) continue;
if (singleRepoUpdate && !repo.getAddress().equals(address)) {
if (singleRepoUpdate && !repo.getAddress().equals(address) && repo.getRepoId() != repoId) {
unchangedRepos++;
continue;
}
@@ -479,9 +487,14 @@ public class UpdateService extends JobIntentService {
// 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);
final long currentRepoId = repo.getRepoId();
final IndexUpdateResult result;
if (repo.getCertificate() == null) {
// This is a new repo without a certificate
result = updater.updateNewRepo(currentRepoId, fingerprint, listener);
} else {
result = updater.update(currentRepoId, repo.getCertificate(), listener);
}
if (result == IndexUpdateResult.UNCHANGED) {
unchangedRepos++;
} else if (result == IndexUpdateResult.PROCESSED) {

View File

@@ -60,7 +60,6 @@ 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;
@@ -343,24 +342,26 @@ public final class Utils {
for (int i = 2; i < fingerprint.length(); i = i + 2) {
displayFP.append(" ").append(fingerprint.substring(i, i + 2));
}
return displayFP.toString();
return displayFP.toString().toUpperCase(Locale.US);
}
@NonNull
public static Uri getLocalRepoUri(Repo repo) {
if (TextUtils.isEmpty(repo.address)) {
public static Uri getLocalRepoUri(Repository repo) {
if (TextUtils.isEmpty(repo.getAddress())) {
return Uri.parse("http://wifi-not-enabled");
}
Uri uri = Uri.parse(repo.address);
Uri uri = Uri.parse(repo.getAddress());
Uri.Builder b = uri.buildUpon();
if (!TextUtils.isEmpty(repo.fingerprint)) {
b.appendQueryParameter("fingerprint", repo.fingerprint);
if (!TextUtils.isEmpty(repo.getCertificate())) {
String fingerprint = DigestUtils.sha256Hex(repo.getCertificate());
b.appendQueryParameter("fingerprint", fingerprint);
}
String scheme = Preferences.get().isLocalRepoHttpsEnabled() ? "https" : "http";
b.scheme(scheme);
return b.build();
}
@Deprecated
public static Uri getSharingUri(Repo repo) {
if (TextUtils.isEmpty(repo.address)) {
return Uri.parse("http://wifi-not-enabled");
@@ -378,6 +379,23 @@ public final class Utils {
return b.build();
}
public static Uri getSharingUri(Repository repo) {
if (TextUtils.isEmpty(repo.getAddress())) {
return Uri.parse("http://wifi-not-enabled");
}
Uri localRepoUri = getLocalRepoUri(repo);
Uri.Builder b = localRepoUri.buildUpon();
b.scheme(localRepoUri.getScheme().replaceFirst("http", "fdroidrepo"));
b.appendQueryParameter("swap", "1");
if (!TextUtils.isEmpty(FDroidApp.bssid)) {
b.appendQueryParameter("bssid", FDroidApp.bssid);
if (!TextUtils.isEmpty(FDroidApp.ssid)) {
b.appendQueryParameter("ssid", FDroidApp.ssid);
}
}
return b.build();
}
/**
* Create a standard {@link PackageManager} {@link Uri} for pointing to an app.
*/

View File

@@ -31,6 +31,12 @@ import android.database.sqlite.SQLiteOpenHelper;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import org.fdroid.database.FDroidDatabase;
import org.fdroid.database.FDroidDatabaseHolder;
import org.fdroid.database.InitialRepository;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
@@ -71,6 +77,28 @@ public class DBHelper extends SQLiteOpenHelper {
private static DBHelper instance;
private static final String DATABASE_NAME = "fdroid";
public static FDroidDatabase getDb(Context context) {
return FDroidDatabaseHolder.getDb(context, "fdroid_db", db -> prePopulateDb(context, db));
}
@WorkerThread
@VisibleForTesting
static void prePopulateDb(Context context, FDroidDatabase db) {
List<String> initialRepos = DBHelper.loadInitialRepos(context);
for (int i = 0; i < initialRepos.size(); i += REPO_XML_ITEM_COUNT) {
InitialRepository repo = new InitialRepository(
initialRepos.get(i), // name
initialRepos.get(i + 1), // address
initialRepos.get(i + 2), // description
initialRepos.get(i + 7), // certificate
Integer.parseInt(initialRepos.get(i + 3)), // version
initialRepos.get(i + 4).equals("1"), // enabled
Integer.parseInt(initialRepos.get(i + 5)) // weight
);
db.getRepositoryDao().insert(repo);
}
}
private static final String CREATE_TABLE_PACKAGE = "CREATE TABLE " + PackageTable.NAME
+ " ( "
+ PackageTable.Cols.PACKAGE_NAME + " text not null, "

View File

@@ -251,7 +251,7 @@ public class RepoProvider extends FDroidProvider {
int appCount = resolver.delete(appUri, null, null);
Utils.debugLog(TAG, "Removed " + appCount + " apps from repo " + repo.address + ".");
AppUpdateStatusManager.getInstance(context).removeAllByRepo(repo);
AppUpdateStatusManager.getInstance(context).removeAllByRepo(repo.id);
AppProvider.Helper.recalculatePreferredMetadata(context);
}

View File

@@ -50,7 +50,6 @@ import com.google.android.material.appbar.MaterialToolbar;
import org.fdroid.database.AppPrefs;
import org.fdroid.database.AppVersion;
import org.fdroid.database.FDroidDatabase;
import org.fdroid.database.FDroidDatabaseHolder;
import org.fdroid.download.DownloadRequest;
import org.fdroid.fdroid.AppUpdateStatusManager;
import org.fdroid.fdroid.CompatibilityChecker;
@@ -159,7 +158,7 @@ public class AppDetailsActivity extends AppCompatActivity
}
);
checker = new CompatibilityChecker(this);
db = FDroidDatabaseHolder.getDb(getApplicationContext());
db = DBHelper.getDb(getApplicationContext());
db.getAppDao().getApp(packageName).observe(this, this::onAppChanged);
db.getVersionDao().getAppVersions(packageName).observe(this, this::onVersionsChanged);
db.getAppPrefsDao().getAppPrefs(packageName).observe(this, this::onAppPrefsChanged);

View File

@@ -22,12 +22,10 @@ package org.fdroid.fdroid.views;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.database.Cursor;
import android.net.Uri;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
@@ -43,10 +41,8 @@ import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
@@ -54,17 +50,21 @@ import com.google.android.material.appbar.MaterialToolbar;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.textfield.TextInputLayout;
import org.fdroid.database.Repository;
import org.fdroid.database.RepositoryDao;
import org.fdroid.download.Mirror;
import org.fdroid.fdroid.AddRepoIntentService;
import org.fdroid.fdroid.AppUpdateStatusManager;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.IndexUpdater;
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.App;
import org.fdroid.fdroid.data.DBHelper;
import org.fdroid.fdroid.data.NewRepoConfig;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.data.Schema.RepoTable;
import java.io.File;
import java.io.IOException;
@@ -72,28 +72,29 @@ import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.Callable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.NavUtils;
import androidx.core.app.TaskStackBuilder;
import androidx.core.content.ContextCompat;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.CursorLoader;
import androidx.loader.content.Loader;
import androidx.recyclerview.widget.RecyclerView;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class ManageReposActivity extends AppCompatActivity
implements LoaderManager.LoaderCallbacks<Cursor>, RepoAdapter.EnabledListener {
public class ManageReposActivity extends AppCompatActivity implements RepoAdapter.RepoItemListener {
private static final String TAG = "ManageReposActivity";
public static final String EXTRA_FINISH_AFTER_ADDING_REPO = "finishAfterAddingRepo";
@@ -102,10 +103,11 @@ public class ManageReposActivity extends AppCompatActivity
private enum AddRepoState {
DOESNT_EXIST, EXISTS_FINGERPRINT_MISMATCH, EXISTS_ADD_MIRROR, EXISTS_ALREADY_MIRROR,
EXISTS_DISABLED, EXISTS_ENABLED, EXISTS_UPGRADABLE_TO_SIGNED, INVALID_URL,
IS_SWAP
EXISTS_DISABLED, EXISTS_ENABLED, EXISTS_UPGRADABLE_TO_SIGNED, INVALID_URL
}
private RepositoryDao repositoryDao;
/**
* True if activity started with an intent such as from QR code. False if
* opened from, e.g. the main menu.
@@ -118,6 +120,7 @@ public class ManageReposActivity extends AppCompatActivity
protected void onCreate(Bundle savedInstanceState) {
FDroidApp fdroidApp = (FDroidApp) getApplication();
fdroidApp.applyPureBlackBackgroundInDarkTheme(this);
repositoryDao = DBHelper.getDb(this).getRepositoryDao();
super.onCreate(savedInstanceState);
@@ -149,17 +152,10 @@ public class ManageReposActivity extends AppCompatActivity
}
});
final ListView repoList = (ListView) findViewById(R.id.list);
repoAdapter = new RepoAdapter(this);
repoAdapter.setEnabledListener(this);
final RecyclerView repoList = (RecyclerView) findViewById(R.id.list);
RepoAdapter repoAdapter = new RepoAdapter(this);
repoList.setAdapter(repoAdapter);
repoList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Repo repo = new Repo((Cursor) repoList.getItemAtPosition(position));
editRepo(repo);
}
});
repositoryDao.getLiveRepositories().observe(this, repoAdapter::updateItems);
}
@Override
@@ -182,9 +178,6 @@ public class ManageReposActivity extends AppCompatActivity
/* let's see if someone is trying to send us a new repo */
addRepoFromIntent(getIntent());
// Starts a new or restarts an existing Loader in this manager
getSupportLoaderManager().restartLoader(0, null, this);
}
@Override
@@ -287,8 +280,8 @@ public class ManageReposActivity extends AppCompatActivity
private class AddRepo {
private final Context context;
private final HashMap<String, Repo> urlRepoMap = new HashMap<>();
private final HashMap<String, Repo> fingerprintRepoMap = new HashMap<>();
private final HashMap<String, Repository> urlRepoMap = new HashMap<>();
private final HashMap<String, Repository> fingerprintRepoMap = new HashMap<>();
private final AlertDialog addRepoDialog;
private final TextView overwriteMessage;
private final ColorStateList defaultTextColour;
@@ -306,14 +299,17 @@ public class ManageReposActivity extends AppCompatActivity
context = ManageReposActivity.this;
for (Repo repo : RepoProvider.Helper.all(context)) {
urlRepoMap.put(repo.address, repo);
for (String url : repo.getMirrorList()) {
urlRepoMap.put(url, repo);
for (Repository repo : FDroidApp.repos) {
urlRepoMap.put(repo.getAddress(), repo);
for (Mirror mirror : repo.getAllMirrors()) {
urlRepoMap.put(mirror.getBaseUrl(), repo);
}
if (!TextUtils.isEmpty(repo.fingerprint)
&& TextUtils.equals(getRepoType(newAddress), getRepoType(repo.address))) {
fingerprintRepoMap.put(repo.fingerprint, repo);
if (!TextUtils.isEmpty(repo.getCertificate())
&& TextUtils.equals(getRepoType(newAddress), getRepoType(repo.getAddress()))) {
String fingerprint = repo.getFingerprint();
if (fingerprint != null) {
fingerprintRepoMap.put(fingerprint.toLowerCase(Locale.ENGLISH), repo);
}
}
}
@@ -376,30 +372,23 @@ public class ManageReposActivity extends AppCompatActivity
String fp = fingerprintEditText.getText().toString();
// remove any whitespace from fingerprint
fp = fp.replaceAll("\\s", "");
fp = fp.replaceAll("\\s", "").toLowerCase(Locale.ENGLISH);
if (TextUtils.isEmpty(fp)) fp = null;
switch (addRepoState) {
case DOESNT_EXIST:
prepareToCreateNewRepo(url, fp, username, password);
break;
case IS_SWAP:
Utils.debugLog(TAG, "Removing existing swap repo " + url
+ " before adding new repo.");
Repo repo = RepoProvider.Helper.findByAddress(context, url);
RepoProvider.Helper.remove(context, repo.getId());
prepareToCreateNewRepo(url, fp, username, password);
break;
case EXISTS_DISABLED:
case EXISTS_UPGRADABLE_TO_SIGNED:
case EXISTS_ADD_MIRROR:
updateAndEnableExistingRepo(url, fp);
finishedAddingRepo();
finishedAddingRepo(url, fp);
break;
default:
finishedAddingRepo();
finishedAddingRepo(url, fp);
break;
}
}
@@ -477,9 +466,10 @@ public class ManageReposActivity extends AppCompatActivity
// Don't bother dealing with this exception yet, as this is called every time
// a letter is added to the repo URL text input. We don't want to display a message
// to the user until they try to save the repo.
return;
}
Repo repo = fingerprintRepoMap.get(fingerprint);
Repository repo = fingerprintRepoMap.get(fingerprint.toLowerCase(Locale.ENGLISH));
if (repo == null) {
repo = urlRepoMap.get(uri);
}
@@ -487,18 +477,17 @@ public class ManageReposActivity extends AppCompatActivity
if (repo == null) {
repoDoesntExist();
} else {
if (repo.isSwap) {
repoIsSwap(repo);
} else if (repo.fingerprint == null && fingerprint.length() > 0) {
if (repo.getFingerprint() == null && fingerprint.length() > 0) {
upgradingToSigned(repo);
} else if (repo.fingerprint != null && !repo.fingerprint.equalsIgnoreCase(fingerprint)) {
} else if (repo.getFingerprint() != null && !repo.getFingerprint().equalsIgnoreCase(fingerprint)) {
repoFingerprintDoesntMatch(repo);
} else {
if (repo.getMirrorList().contains(uri) && !TextUtils.equals(repo.address, uri) && repo.inuse) {
Repository mirrorRepo = urlRepoMap.get(uri);
if (repo.equals(mirrorRepo) && !TextUtils.equals(repo.getAddress(), uri) && repo.getEnabled()) {
repoExistsAlreadyMirror(repo);
} else if (!TextUtils.equals(repo.address, uri) && repo.inuse) {
} else if (!TextUtils.equals(repo.getAddress(), uri) && repo.getEnabled()) {
repoExistsAddMirror(repo);
} else if (repo.inuse) {
} else if (repo.getEnabled()) {
repoExistsAndEnabled(repo);
} else {
repoExistsAndDisabled(repo);
@@ -511,15 +500,11 @@ public class ManageReposActivity extends AppCompatActivity
updateUi(null, AddRepoState.DOESNT_EXIST, 0, false, R.string.repo_add_add, true);
}
private void repoIsSwap(Repo repo) {
updateUi(repo, AddRepoState.IS_SWAP, 0, false, R.string.repo_add_add, true);
}
/**
* Same address with different fingerprint, this could be malicious, so display a message
* force the user to manually delete the repo before adding this one.
*/
private void repoFingerprintDoesntMatch(Repo repo) {
private void repoFingerprintDoesntMatch(Repository repo) {
updateUi(repo, AddRepoState.EXISTS_FINGERPRINT_MISMATCH,
R.string.repo_delete_to_overwrite,
true, R.string.overwrite, false);
@@ -530,32 +515,32 @@ public class ManageReposActivity extends AppCompatActivity
R.string.repo_add_add, false);
}
private void repoExistsAndDisabled(Repo repo) {
private void repoExistsAndDisabled(Repository repo) {
updateUi(repo, AddRepoState.EXISTS_DISABLED,
R.string.repo_exists_enable, false, R.string.enable, true);
}
private void repoExistsAndEnabled(Repo repo) {
private void repoExistsAndEnabled(Repository repo) {
updateUi(repo, AddRepoState.EXISTS_ENABLED, R.string.repo_exists_and_enabled, false,
R.string.ok, true);
}
private void repoExistsAddMirror(Repo repo) {
private void repoExistsAddMirror(Repository repo) {
updateUi(repo, AddRepoState.EXISTS_ADD_MIRROR, R.string.repo_exists_add_mirror, false,
R.string.repo_add_mirror, true);
}
private void repoExistsAlreadyMirror(Repo repo) {
private void repoExistsAlreadyMirror(Repository repo) {
updateUi(repo, AddRepoState.EXISTS_ALREADY_MIRROR, 0, false, R.string.ok, true);
}
private void upgradingToSigned(Repo repo) {
private void upgradingToSigned(Repository repo) {
updateUi(repo, AddRepoState.EXISTS_UPGRADABLE_TO_SIGNED, R.string.repo_exists_add_fingerprint,
false, R.string.add_key, true);
}
private void updateUi(Repo repo, AddRepoState state, int messageRes, boolean redMessage, int addTextRes,
boolean addEnabled) {
private void updateUi(@Nullable Repository repo, AddRepoState state, int messageRes, boolean redMessage,
int addTextRes, boolean addEnabled) {
if (addRepoState != state) {
addRepoState = state;
@@ -563,7 +548,7 @@ public class ManageReposActivity extends AppCompatActivity
if (repo == null) {
name = '"' + getString(R.string.unknown) + '"';
} else {
name = repo.name;
name = repo.getName(App.getLocales());
}
if (messageRes > 0) {
@@ -582,10 +567,11 @@ public class ManageReposActivity extends AppCompatActivity
addButton.setText(addTextRes);
addButton.setEnabled(addEnabled);
if (Build.VERSION.SDK_INT >= 15 && addRepoState == AddRepoState.EXISTS_ALREADY_MIRROR) {
if (addRepoState == AddRepoState.EXISTS_ALREADY_MIRROR) {
addButton.callOnClick();
editRepo(repo);
String msg = getString(R.string.repo_exists_and_enabled, repo.address);
if (repo != null) editRepo(repo);
Objects.requireNonNull(repo); // should be non-null in this addRepoState
String msg = getString(R.string.repo_exists_and_enabled, repo.getAddress());
Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
}
}
@@ -594,7 +580,7 @@ public class ManageReposActivity extends AppCompatActivity
/**
* Adds a new repo to the database.
*/
private void prepareToCreateNewRepo(final String originalAddress, final String fingerprint,
private void prepareToCreateNewRepo(final String originalAddress, @Nullable final String fingerprint,
final String username, final String password) {
final View addRepoForm = addRepoDialog.findViewById(R.id.add_repo_form);
addRepoForm.setVisibility(View.GONE);
@@ -702,7 +688,7 @@ public class ManageReposActivity extends AppCompatActivity
textSearching.setText("");
skip.setText(R.string.cancel);
skip.setOnClickListener(null);
validateRepoDetails(newAddress, fingerprint);
validateRepoDetails(newAddress, fingerprint == null ? "" : fingerprint);
} else {
// create repo without username/password
createNewRepo(newAddress, fingerprint);
@@ -725,77 +711,73 @@ public class ManageReposActivity extends AppCompatActivity
/**
* Create a repository without a username or password.
*/
private void createNewRepo(String address, String fingerprint) {
private void createNewRepo(String address, @Nullable String fingerprint) {
createNewRepo(address, fingerprint, null, null);
}
private void createNewRepo(String address, String fingerprint,
private void createNewRepo(String address, @Nullable String fingerprint,
final String username, final String password) {
try {
address = AddRepoIntentService.normalizeUrl(address);
} catch (URISyntaxException e) {
// Leave address as it was.
}
ContentValues values = new ContentValues(4);
values.put(RepoTable.Cols.ADDRESS, address);
if (!TextUtils.isEmpty(fingerprint)) {
values.put(RepoTable.Cols.FINGERPRINT, fingerprint.toUpperCase(Locale.ENGLISH));
}
if (address == null) return;
if (!TextUtils.isEmpty(username) && !TextUtils.isEmpty(password)) {
values.put(RepoTable.Cols.USERNAME, username);
values.put(RepoTable.Cols.PASSWORD, password);
}
final String repoAddress = address;
RepoProvider.Helper.insert(context, values);
finishedAddingRepo();
Toast.makeText(context, getString(R.string.repo_added, address), Toast.LENGTH_SHORT).show();
Disposable disposable = Single.fromCallable(
() -> repositoryDao.insertEmptyRepo(repoAddress, username, password)
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(repoId -> {
finishedAddingRepo(repoAddress, fingerprint);
Toast.makeText(context, getString(R.string.repo_added, repoAddress), Toast.LENGTH_SHORT)
.show();
});
compositeDisposable.add(disposable);
}
/**
* Seeing as this repo already exists, we will force it to be enabled again.
*/
private void updateAndEnableExistingRepo(String url, String fingerprint) {
private void updateAndEnableExistingRepo(String url, @Nullable String fingerprint) {
if (fingerprint != null) {
fingerprint = fingerprint.trim();
if (TextUtils.isEmpty(fingerprint)) {
fingerprint = null;
} else {
fingerprint = fingerprint.toUpperCase(Locale.ENGLISH);
fingerprint = fingerprint.toLowerCase(Locale.ENGLISH);
}
}
Utils.debugLog(TAG, "Enabling existing repo: " + url);
Repo repo = fingerprintRepoMap.get(fingerprint);
Repository repo = fingerprintRepoMap.get(fingerprint);
if (repo == null) {
repo = RepoProvider.Helper.findByAddress(context, url);
repo = urlRepoMap.get(url);
}
ContentValues values = new ContentValues(2);
values.put(RepoTable.Cols.IN_USE, 1);
values.put(RepoTable.Cols.FINGERPRINT, fingerprint);
if (!TextUtils.equals(url, repo.address)) {
boolean addUserMirror = true;
for (String mirror : repo.getMirrorList()) {
if (TextUtils.equals(mirror, url)) {
addUserMirror = false;
}
}
if (addUserMirror) {
if (repo.userMirrors == null) {
repo.userMirrors = new String[]{url};
} else {
int last = repo.userMirrors.length;
repo.userMirrors = Arrays.copyOf(repo.userMirrors, last + 1);
repo.userMirrors[last] = url;
}
values.put(RepoTable.Cols.USER_MIRRORS, Utils.serializeCommaSeparatedString(repo.userMirrors));
// return if this repo is gone
if (repo == null) return;
// return if a repo with that exact same address already exists
if (TextUtils.equals(url, repo.getAddress())) return;
// return if this address is already a mirror
for (Mirror mirror : repo.getAllMirrors()) {
if (TextUtils.equals(mirror.getBaseUrl(), url)) {
return;
}
}
RepoProvider.Helper.update(context, repo, values);
ArrayList<String> userMirrors = new ArrayList<>(repo.getUserMirrors());
userMirrors.add(url);
final long repoId = repo.getRepoId();
runOffUiThread(() -> {
repositoryDao.updateUserMirrors(repoId, userMirrors);
return true;
});
// TODO does this change get reflected?
notifyDataSetChanged();
finishedAddingRepo();
finishedAddingRepo(url, fingerprint);
}
/**
@@ -803,8 +785,9 @@ public class ManageReposActivity extends AppCompatActivity
* will set a result and finish. Otherwise, we'll updateViews the list of repos
* to reflect the newly created repo.
*/
private void finishedAddingRepo() {
UpdateService.updateNow(ManageReposActivity.this);
private void finishedAddingRepo(String address, @Nullable String fingerprint) {
String f = fingerprint == null ? null : fingerprint.toLowerCase(Locale.ENGLISH);
UpdateService.updateNewRepoNow(ManageReposActivity.this, address, f);
if (addRepoDialog.isShowing()) {
addRepoDialog.dismiss();
}
@@ -849,30 +832,9 @@ public class ManageReposActivity extends AppCompatActivity
}
}
private RepoAdapter repoAdapter;
@NonNull
@Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
Uri uri = RepoProvider.allExceptSwapUri();
final String[] projection = {
RepoTable.Cols._ID,
RepoTable.Cols.NAME,
RepoTable.Cols.SIGNING_CERT,
RepoTable.Cols.FINGERPRINT,
RepoTable.Cols.IN_USE,
};
return new CursorLoader(this, uri, projection, null, null, null);
}
@Override
public void onLoadFinished(@NonNull Loader<Cursor> cursorLoader, Cursor cursor) {
repoAdapter.swapCursor(cursor);
}
@Override
public void onLoaderReset(@NonNull Loader<Cursor> cursorLoader) {
repoAdapter.swapCursor(null);
public void onClicked(Repository repo) {
editRepo(repo);
}
/**
@@ -891,17 +853,19 @@ public class ManageReposActivity extends AppCompatActivity
* update the repos if you toggled on on.
*/
@Override
public void onSetEnabled(Repo repo, boolean isEnabled) {
if (repo.inuse != isEnabled) {
ContentValues values = new ContentValues(1);
values.put(RepoTable.Cols.IN_USE, isEnabled ? 1 : 0);
RepoProvider.Helper.update(this, repo, values);
public void onSetEnabled(Repository repo, boolean isEnabled) {
if (repo.getEnabled() != isEnabled) {
runOffUiThread(() -> {
repositoryDao.setRepositoryEnabled(repo.getRepoId(), isEnabled);
return true;
});
if (isEnabled) {
UpdateService.updateNow(this);
UpdateService.updateRepoNow(this, repo.getAddress());
} else {
RepoProvider.Helper.purgeApps(this, repo);
String notification = getString(R.string.repo_disabled_notification, repo.name);
AppUpdateStatusManager.getInstance(this).removeAllByRepo(repo.getRepoId());
// RepoProvider.Helper.purgeApps(this, repo);
String notification = getString(R.string.repo_disabled_notification, repo.getName(App.getLocales()));
Toast.makeText(this, notification, Toast.LENGTH_LONG).show();
}
}
@@ -909,9 +873,9 @@ public class ManageReposActivity extends AppCompatActivity
public static final int SHOW_REPO_DETAILS = 1;
public void editRepo(Repo repo) {
public void editRepo(Repository repo) {
Intent intent = new Intent(this, RepoDetailsActivity.class);
intent.putExtra(RepoDetailsActivity.ARG_REPO_ID, repo.getId());
intent.putExtra(RepoDetailsActivity.ARG_REPO_ID, repo.getRepoId());
startActivityForResult(intent, SHOW_REPO_DETAILS);
}
@@ -922,7 +886,15 @@ public class ManageReposActivity extends AppCompatActivity
* repo, and wanting the switch to be changed to on).
*/
private void notifyDataSetChanged() {
getSupportLoaderManager().restartLoader(0, null, this);
// TODO still needed?
}
private <T> void runOffUiThread(Callable<T> r) {
Disposable disposable = Single.fromCallable(r)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe();
compositeDisposable.add(disposable);
}
/**

View File

@@ -1,92 +1,105 @@
package org.fdroid.fdroid.views;
import android.content.Context;
import android.database.Cursor;
import android.annotation.SuppressLint;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CompoundButton;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import org.fdroid.database.Repository;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.compat.CursorAdapterCompat;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.App;
import androidx.cursoradapter.widget.CursorAdapter;
import java.util.ArrayList;
import java.util.List;
public class RepoAdapter extends CursorAdapter {
public class RepoAdapter extends RecyclerView.Adapter<RepoAdapter.RepoViewHolder> {
public interface EnabledListener {
void onSetEnabled(Repo repo, boolean isEnabled);
public interface RepoItemListener {
void onClicked(Repository repo);
void onSetEnabled(Repository repo, boolean isEnabled);
}
private final LayoutInflater inflater;
private final List<Repository> items = new ArrayList<>();
private final RepoItemListener repoItemListener;
private EnabledListener enabledListener;
RepoAdapter(Context context) {
super(context, null, CursorAdapterCompat.FLAG_AUTO_REQUERY);
inflater = LayoutInflater.from(context);
RepoAdapter(RepoItemListener repoItemListener) {
this.repoItemListener = repoItemListener;
}
public void setEnabledListener(EnabledListener listener) {
enabledListener = listener;
@SuppressLint("NotifyDataSetChanged") // we could do better, but not really worth it at this point
public void updateItems(List<Repository> items) {
this.items.clear();
this.items.addAll(items);
notifyDataSetChanged();
}
@NonNull
@Override
public RepoViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View v = inflater.inflate(R.layout.repo_item, parent, false);
return new RepoViewHolder(v);
}
@Override
public boolean hasStableIds() {
return true;
public void onBindViewHolder(@NonNull RepoViewHolder holder, int position) {
holder.bind(items.get(position));
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
View view = inflater.inflate(R.layout.repo_item, parent, false);
setupView(cursor, view, (CompoundButton) view.findViewById(R.id.repo_switch));
return view;
public int getItemCount() {
return items.size();
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
CompoundButton switchView = (CompoundButton) view.findViewById(R.id.repo_switch);
class RepoViewHolder extends RecyclerView.ViewHolder {
private final View rootView;
private final CompoundButton switchView;
private final TextView nameView;
private final View unsignedView;
private final View unverifiedView;
// Remove old listener (because we are reusing this view, we don't want
// to invoke the listener for the last repo to use it - particularly
// because we are potentially about to change the checked status
// which would in turn invoke this listener....
switchView.setOnCheckedChangeListener(null);
setupView(cursor, view, switchView);
}
RepoViewHolder(@NonNull View view) {
super(view);
rootView = view;
switchView = view.findViewById(R.id.repo_switch);
nameView = view.findViewById(R.id.repo_name);
unsignedView = view.findViewById(R.id.repo_unsigned);
unverifiedView = view.findViewById(R.id.repo_unverified);
}
private void setupView(Cursor cursor, View view, CompoundButton switchView) {
final Repo repo = new Repo(cursor);
private void bind(Repository repo) {
rootView.setOnClickListener(v -> repoItemListener.onClicked(repo));
// Remove old listener (because we are reusing this view, we don't want
// to invoke the listener for the last repo to use it - particularly
// because we are potentially about to change the checked status
// which would in turn invoke this listener....
switchView.setOnCheckedChangeListener(null);
switchView.setChecked(repo.getEnabled());
switchView.setChecked(repo.inuse);
// Add this listener *after* setting the checked status, so we don't
// invoke the listener while setting up the view...
switchView.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (enabledListener != null) {
enabledListener.onSetEnabled(repo, isChecked);
// Add this listener *after* setting the checked status, so we don't
// invoke the listener while setting up the view...
switchView.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (repoItemListener != null) {
repoItemListener.onSetEnabled(repo, isChecked);
}
});
nameView.setText(repo.getName(App.getLocales()));
if (repo.getCertificate() != null) {
unsignedView.setVisibility(View.GONE);
unverifiedView.setVisibility(View.GONE);
} else if (repo.getCertificate() == null) { // FIXME: Do we still need that unsignedView at all?
unsignedView.setVisibility(View.GONE);
unverifiedView.setVisibility(View.VISIBLE);
} else {
unsignedView.setVisibility(View.VISIBLE);
unverifiedView.setVisibility(View.GONE);
}
});
TextView nameView = (TextView) view.findViewById(R.id.repo_name);
nameView.setText(repo.getName());
View unsignedView = view.findViewById(R.id.repo_unsigned);
View unverifiedView = view.findViewById(R.id.repo_unverified);
if (repo.isSigned()) {
unsignedView.setVisibility(View.GONE);
unverifiedView.setVisibility(View.GONE);
} else if (repo.isSignedButUnverified()) {
unsignedView.setVisibility(View.GONE);
unverifiedView.setVisibility(View.VISIBLE);
} else {
unsignedView.setVisibility(View.VISIBLE);
unverifiedView.setVisibility(View.GONE);
}
}
}

View File

@@ -2,7 +2,6 @@ package org.fdroid.fdroid.views;
import android.annotation.TargetApi;
import android.content.BroadcastReceiver;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
@@ -30,19 +29,24 @@ import android.widget.Toast;
import com.google.android.material.appbar.MaterialToolbar;
import com.google.android.material.textfield.TextInputLayout;
import org.fdroid.database.AppDao;
import org.fdroid.database.Repository;
import org.fdroid.database.RepositoryDao;
import org.fdroid.download.Mirror;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.NfcHelper;
import org.fdroid.fdroid.NfcNotEnabledActivity;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.UpdateService;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.data.Schema.RepoTable;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.DBHelper;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.Callable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -53,7 +57,11 @@ import androidx.core.content.ContextCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class RepoDetailsActivity extends AppCompatActivity {
private static final String TAG = "RepoDetailsActivity";
@@ -86,13 +94,15 @@ public class RepoDetailsActivity extends AppCompatActivity {
private static final int[] HIDE_IF_EXISTS = {
R.id.text_not_yet_updated,
};
private Repo repo;
private Repository repo;
private long repoId;
private View repoView;
private String shareUrl;
private MirrorAdapter adapterToNotify;
private RepositoryDao repositoryDao;
private AppDao appDao;
@Nullable
private Disposable disposable;
@@ -112,6 +122,8 @@ public class RepoDetailsActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
FDroidApp fdroidApp = (FDroidApp) getApplication();
fdroidApp.applyPureBlackBackgroundInDarkTheme(this);
repositoryDao = DBHelper.getDb(this).getRepositoryDao();
appDao = DBHelper.getDb(this).getAppDao();
super.onCreate(savedInstanceState);
@@ -124,27 +136,31 @@ public class RepoDetailsActivity extends AppCompatActivity {
repoView = findViewById(R.id.repo_view);
repoId = getIntent().getLongExtra(ARG_REPO_ID, 0);
repo = RepoProvider.Helper.findById(this, repoId);
repo = FDroidApp.getRepo(repoId);
TextView inputUrl = findViewById(R.id.input_repo_url);
inputUrl.setText(repo.address);
inputUrl.setText(repo.getAddress());
RecyclerView officialMirrorListView = findViewById(R.id.official_mirror_list);
officialMirrorListView.setLayoutManager(new LinearLayoutManager(this));
adapterToNotify = new MirrorAdapter(repo, repo.mirrors);
adapterToNotify = new MirrorAdapter(repo, repo.getAllMirrors(false));
officialMirrorListView.setAdapter(adapterToNotify);
RecyclerView userMirrorListView = findViewById(R.id.user_mirror_list);
userMirrorListView.setLayoutManager(new LinearLayoutManager(this));
userMirrorListView.setAdapter(new MirrorAdapter(repo, repo.userMirrors));
MirrorAdapter userMirrorAdapter = new MirrorAdapter(repo, repo.getUserMirrors().size());
userMirrorAdapter.setUserMirrors(repo.getUserMirrors());
userMirrorListView.setAdapter(userMirrorAdapter);
if (repo.address.startsWith("content://") || repo.address.startsWith("file://")) {
if (repo.getAddress().startsWith("content://") || repo.getAddress().startsWith("file://")) {
// no need to show a QR Code, it is not shareable
return;
}
Uri uri = Uri.parse(repo.address);
uri = uri.buildUpon().appendQueryParameter("fingerprint", repo.fingerprint).build();
Uri uri = Uri.parse(repo.getAddress());
if (repo.getFingerprint() != null) {
uri = uri.buildUpon().appendQueryParameter("fingerprint", repo.getFingerprint()).build();
}
String qrUriString = uri.toString();
disposable = Utils.generateQrBitmap(this, qrUriString)
.subscribe(bitmap -> {
@@ -183,7 +199,7 @@ public class RepoDetailsActivity extends AppCompatActivity {
* have been updated. The safest way to deal with this is to reload the
* repo object directly from the database.
*/
repo = RepoProvider.Helper.findById(this, repoId);
repo = FDroidApp.getRepo(repoId);
updateRepoView();
LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver,
@@ -290,12 +306,12 @@ public class RepoDetailsActivity extends AppCompatActivity {
}
private void prepareShareMenuItems(Menu menu) {
if (!TextUtils.isEmpty(repo.address)) {
if (!TextUtils.isEmpty(repo.fingerprint)) {
shareUrl = Uri.parse(repo.address).buildUpon()
.appendQueryParameter("fingerprint", repo.fingerprint).toString();
if (!TextUtils.isEmpty(repo.getAddress())) {
if (!TextUtils.isEmpty(repo.getCertificate())) {
shareUrl = Uri.parse(repo.getAddress()).buildUpon()
.appendQueryParameter("fingerprint", repo.getFingerprint()).toString();
} else {
shareUrl = repo.address;
shareUrl = repo.getAddress();
}
menu.findItem(R.id.action_share).setVisible(true);
} else {
@@ -303,51 +319,52 @@ public class RepoDetailsActivity extends AppCompatActivity {
}
}
private void setupDescription(View parent, Repo repo) {
private void setupDescription(View parent, Repository repo) {
TextView descriptionLabel = (TextView) parent.findViewById(R.id.label_description);
TextView description = (TextView) parent.findViewById(R.id.text_description);
if (TextUtils.isEmpty(repo.description)) {
String desc = repo.getDescription(App.getLocales());
if (desc == null || TextUtils.isEmpty(desc)) {
descriptionLabel.setVisibility(View.GONE);
description.setVisibility(View.GONE);
description.setText("");
} else {
descriptionLabel.setVisibility(View.VISIBLE);
description.setVisibility(View.VISIBLE);
description.setText(repo.description.replaceAll("\n", " "));
description.setText(desc.replaceAll("\n", " "));
}
}
private void setupRepoFingerprint(View parent, Repo repo) {
private void setupRepoFingerprint(View parent, Repository repo) {
TextView repoFingerprintView = (TextView) parent.findViewById(R.id.text_repo_fingerprint);
TextView repoFingerprintDescView = (TextView) parent.findViewById(R.id.text_repo_fingerprint_description);
String repoFingerprint;
// TODO show the current state of the signature check, not just whether there is a key or not
if (TextUtils.isEmpty(repo.fingerprint) && TextUtils.isEmpty(repo.signingCertificate)) {
if (TextUtils.isEmpty(repo.getCertificate())) {
repoFingerprint = getResources().getString(R.string.unsigned);
repoFingerprintView.setTextColor(ContextCompat.getColor(this, R.color.unsigned));
repoFingerprintDescView.setVisibility(View.VISIBLE);
repoFingerprintDescView.setText(getResources().getString(R.string.unsigned_description));
} else {
// this is based on repo.fingerprint always existing, which it should
repoFingerprint = Utils.formatFingerprint(this, repo.fingerprint);
repoFingerprint = Utils.formatFingerprint(this, repo.getFingerprint());
repoFingerprintDescView.setVisibility(View.GONE);
}
repoFingerprintView.setText(repoFingerprint);
}
private void setupCredentials(View parent, Repo repo) {
private void setupCredentials(View parent, Repository repo) {
TextView usernameLabel = parent.findViewById(R.id.label_username);
TextView username = parent.findViewById(R.id.text_username);
Button changePassword = parent.findViewById(R.id.button_edit_credentials);
changePassword.setOnClickListener(this::showChangePasswordDialog);
if (TextUtils.isEmpty(repo.username)) {
if (TextUtils.isEmpty(repo.getUsername())) {
usernameLabel.setVisibility(View.GONE);
username.setVisibility(View.GONE);
username.setText("");
@@ -355,7 +372,7 @@ public class RepoDetailsActivity extends AppCompatActivity {
} else {
usernameLabel.setVisibility(View.VISIBLE);
username.setVisibility(View.VISIBLE);
username.setText(repo.username);
username.setText(repo.getUsername());
changePassword.setVisibility(View.VISIBLE);
}
}
@@ -363,8 +380,7 @@ public class RepoDetailsActivity extends AppCompatActivity {
private void updateRepoView() {
TextView officialMirrorsLabel = repoView.findViewById(R.id.label_official_mirrors);
RecyclerView officialMirrorList = repoView.findViewById(R.id.official_mirror_list);
if ((repo.mirrors != null && repo.mirrors.length > 1)
|| (repo.userMirrors != null && repo.userMirrors.length > 0)) {
if (repo.getAllMirrors().size() > 1) {
// don't show this if there is only the canonical URL available, and no other mirrors
officialMirrorsLabel.setVisibility(View.VISIBLE);
officialMirrorList.setVisibility(View.VISIBLE);
@@ -375,7 +391,7 @@ public class RepoDetailsActivity extends AppCompatActivity {
TextView userMirrorsLabel = repoView.findViewById(R.id.label_user_mirrors);
RecyclerView userMirrorList = repoView.findViewById(R.id.user_mirror_list);
if (repo.userMirrors != null && repo.userMirrors.length > 0) {
if (repo.getUserMirrors().size() > 0) {
userMirrorsLabel.setVisibility(View.VISIBLE);
userMirrorList.setVisibility(View.VISIBLE);
} else {
@@ -383,7 +399,7 @@ public class RepoDetailsActivity extends AppCompatActivity {
userMirrorList.setVisibility(View.GONE);
}
if (repo.hasBeenUpdated()) {
if (repo.getLastETag() != null) {
updateViewForExistingRepo(repoView);
} else {
setMultipleViewVisibility(repoView, HIDE_IF_EXISTS, View.VISIBLE);
@@ -399,10 +415,11 @@ public class RepoDetailsActivity extends AppCompatActivity {
TextView numApps = repoView.findViewById(R.id.text_num_apps);
TextView lastUpdated = repoView.findViewById(R.id.text_last_update);
name.setText(repo.name);
int appCount = RepoProvider.Helper.countAppsForRepo(this, repoId);
numApps.setText(String.format(Locale.getDefault(), "%d", appCount));
name.setText(repo.getName(App.getLocales()));
disposable = Single.fromCallable(() -> appDao.getNumberOfAppsInRepository(repoId))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(appCount -> numApps.setText(String.format(Locale.getDefault(), "%d", appCount)));
setupDescription(repoView, repo);
setupRepoFingerprint(repoView, repo);
@@ -410,14 +427,13 @@ public class RepoDetailsActivity extends AppCompatActivity {
// Repos that existed before this feature was supported will have an
// "Unknown" last update until next time they update...
if (repo.lastUpdated == null) {
if (repo.getLastUpdated() == null) {
lastUpdated.setText(R.string.unknown);
} else {
int format = DateUtils.isToday(repo.lastUpdated.getTime()) ?
int format = DateUtils.isToday(repo.getLastUpdated()) ?
DateUtils.FORMAT_SHOW_TIME :
DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE;
lastUpdated.setText(DateUtils.formatDateTime(this,
repo.lastUpdated.getTime(), format));
lastUpdated.setText(DateUtils.formatDateTime(this, repo.getLastUpdated(), format));
}
}
@@ -428,7 +444,10 @@ public class RepoDetailsActivity extends AppCompatActivity {
.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
RepoProvider.Helper.remove(getApplicationContext(), repoId);
runOffUiThread(() -> {
repositoryDao.deleteRepository(repoId);
return true;
});
finish();
}
}).setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
@@ -448,7 +467,7 @@ public class RepoDetailsActivity extends AppCompatActivity {
final EditText nameInput = nameInputLayout.getEditText();
final EditText passwordInput = passwordInputLayout.getEditText();
nameInput.setText(repo.username);
nameInput.setText(repo.getUsername());
passwordInput.requestFocus();
credentialsDialog.setTitle(R.string.repo_edit_credentials);
@@ -471,19 +490,13 @@ public class RepoDetailsActivity extends AppCompatActivity {
final String password = passwordInput.getText().toString();
if (!TextUtils.isEmpty(name)) {
final ContentValues values = new ContentValues(2);
values.put(RepoTable.Cols.USERNAME, name);
values.put(RepoTable.Cols.PASSWORD, password);
RepoProvider.Helper.update(RepoDetailsActivity.this, repo, values);
runOffUiThread(() -> {
repositoryDao.updateUsernameAndPassword(repo.getRepoId(), name, password);
return true;
});
updateRepoView();
dialog.dismiss();
} else {
Toast.makeText(RepoDetailsActivity.this, R.string.repo_error_empty_username,
Toast.LENGTH_LONG).show();
}
@@ -494,8 +507,9 @@ public class RepoDetailsActivity extends AppCompatActivity {
}
private class MirrorAdapter extends RecyclerView.Adapter<MirrorAdapter.MirrorViewHolder> {
private final Repo repo;
private final String[] mirrors;
private final Repository repo;
private final List<Mirror> mirrors;
private final HashSet<String> disabledMirrors;
class MirrorViewHolder extends RecyclerView.ViewHolder {
View view;
@@ -506,9 +520,22 @@ public class RepoDetailsActivity extends AppCompatActivity {
}
}
MirrorAdapter(Repo repo, String[] mirrors) {
MirrorAdapter(Repository repo, List<Mirror> mirrors) {
this.repo = repo;
this.mirrors = mirrors;
disabledMirrors = new HashSet<>(repo.getDisabledMirrors());
}
MirrorAdapter(Repository repo, int userMirrorSize) {
this.repo = repo;
this.mirrors = new ArrayList<>(userMirrorSize);
disabledMirrors = new HashSet<>(repo.getDisabledMirrors());
}
void setUserMirrors(List<String> userMirrors) {
for (String url : userMirrors) {
this.mirrors.add(new Mirror(url));
}
}
@NonNull
@@ -521,16 +548,15 @@ public class RepoDetailsActivity extends AppCompatActivity {
@Override
public void onBindViewHolder(@NonNull MirrorViewHolder holder, final int position) {
TextView repoNameTextView = holder.view.findViewById(R.id.repo_name);
repoNameTextView.setText(mirrors[position]);
Mirror mirror = mirrors.get(position);
repoNameTextView.setText(mirror.getBaseUrl());
final String itemMirror = mirrors[position];
final String itemMirror = mirror.getBaseUrl();
boolean enabled = true;
if (repo.disabledMirrors != null) {
for (String disabled : repo.disabledMirrors) {
if (TextUtils.equals(itemMirror, disabled)) {
enabled = false;
break;
}
for (String disabled : repo.getDisabledMirrors()) {
if (TextUtils.equals(itemMirror, disabled)) {
enabled = false;
break;
}
}
CompoundButton switchView = holder.view.findViewById(R.id.repo_switch);
@@ -538,36 +564,23 @@ public class RepoDetailsActivity extends AppCompatActivity {
switchView.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
HashSet<String> disabledMirrors;
if (repo.disabledMirrors == null) {
disabledMirrors = new HashSet<>(1);
} else {
disabledMirrors = new HashSet<>(Arrays.asList(repo.disabledMirrors));
}
if (isChecked) {
disabledMirrors.remove(itemMirror);
} else {
disabledMirrors.add(itemMirror);
}
int totalMirrors = (repo.mirrors == null ? 0 : repo.mirrors.length)
+ (repo.userMirrors == null ? 0 : repo.userMirrors.length);
List<Mirror> mirrors = repo.getAllMirrors(true);
int totalMirrors = mirrors.size();
if (disabledMirrors.size() == totalMirrors) {
// if all mirrors are disabled, re-enable canonical repo as mirror
disabledMirrors.remove(repo.address);
disabledMirrors.remove(repo.getAddress());
adapterToNotify.notifyItemChanged(0);
}
if (disabledMirrors.size() == 0) {
repo.disabledMirrors = null;
} else {
repo.disabledMirrors = disabledMirrors.toArray(new String[disabledMirrors.size()]);
}
final ContentValues values = new ContentValues(1);
values.put(RepoTable.Cols.DISABLED_MIRRORS,
Utils.serializeCommaSeparatedString(repo.disabledMirrors));
RepoProvider.Helper.update(RepoDetailsActivity.this, repo, values);
runOffUiThread(() -> {
repositoryDao.updateDisabledMirrors(repo.getRepoId(), new ArrayList<>(disabledMirrors));
return true;
});
}
});
@@ -583,7 +596,14 @@ public class RepoDetailsActivity extends AppCompatActivity {
if (mirrors == null) {
return 0;
}
return mirrors.length;
return mirrors.size();
}
}
private void runOffUiThread(Callable<?> r) {
disposable = Single.fromCallable(r)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe();
}
}

View File

@@ -24,8 +24,8 @@ 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.data.DBHelper;
import org.fdroid.fdroid.views.apps.AppListActivity;
import org.fdroid.fdroid.views.apps.FeatureImage;
@@ -57,7 +57,7 @@ public class CategoryController extends RecyclerView.ViewHolder {
super(itemView);
this.activity = activity;
db = FDroidDatabaseHolder.getDb(activity);
db = DBHelper.getDb(activity);
appCardsAdapter = new AppPreviewAdapter(activity);

View File

@@ -3,6 +3,7 @@
android:layout_width="match_parent"
android:layout_height="?attr/listPreferredItemHeight"
android:orientation="horizontal"
android:background="?attr/selectableItemBackground"
android:gravity="center_vertical"
android:paddingLeft="?attr/listPreferredItemPaddingLeft"
android:paddingStart="?attr/listPreferredItemPaddingLeft"

View File

@@ -23,11 +23,12 @@
</com.google.android.material.appbar.AppBarLayout>
<ListView
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/repo_item" />
</LinearLayout>