diff --git a/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java b/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java index 07df7502d..4444c88f3 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java @@ -97,6 +97,7 @@ public class DBHelper extends SQLiteOpenHelper { + RepoTable.Cols.ICON + " string, " + RepoTable.Cols.MIRRORS + " string, " + RepoTable.Cols.USER_MIRRORS + " string, " + + RepoTable.Cols.DISABLED_MIRRORS + " string, " + RepoTable.Cols.PUSH_REQUESTS + " integer not null default " + Repo.PUSH_REQUEST_IGNORE + ");"; @@ -223,7 +224,7 @@ public class DBHelper extends SQLiteOpenHelper { + "primary key(" + ApkAntiFeatureJoinTable.Cols.APK_ID + ", " + ApkAntiFeatureJoinTable.Cols.ANTI_FEATURE_ID + ") " + " );"; - protected static final int DB_VERSION = 79; + protected static final int DB_VERSION = 80; private final Context context; @@ -448,6 +449,17 @@ public class DBHelper extends SQLiteOpenHelper { addLiberapayID(db, oldVersion); addUserMirrorsFields(db, oldVersion); removeNotNullFromVersionName(db, oldVersion); + addDisabledMirrorsFields(db, oldVersion); + } + + private void addDisabledMirrorsFields(SQLiteDatabase db, int oldVersion) { + if (oldVersion >= 80) { + return; + } + if (!columnExists(db, RepoTable.NAME, RepoTable.Cols.DISABLED_MIRRORS)) { + Utils.debugLog(TAG, "Adding " + RepoTable.Cols.DISABLED_MIRRORS + " field to " + RepoTable.NAME + " table in db."); + db.execSQL("alter table " + RepoTable.NAME + " add column " + RepoTable.Cols.DISABLED_MIRRORS + " string;"); + } } private void removeNotNullFromVersionName(SQLiteDatabase db, int oldVersion) { diff --git a/app/src/main/java/org/fdroid/fdroid/data/Repo.java b/app/src/main/java/org/fdroid/fdroid/data/Repo.java index 2bebb1854..fa92350ca 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/Repo.java +++ b/app/src/main/java/org/fdroid/fdroid/data/Repo.java @@ -86,6 +86,9 @@ public class Repo extends ValueObject { @JsonIgnore public int pushRequests = PUSH_REQUEST_IGNORE; + /** + * The canonical URL of the repo. + */ public String address; public String name; public String description; @@ -125,8 +128,15 @@ public class Repo extends ValueObject { /** * Mirrors added by the user, either by UI input or by attaching removeable storage */ + @JsonIgnore public String[] userMirrors; + /** + * Mirrors that have been manually disabled by the user. + */ + @JsonIgnore + public String[] disabledMirrors; + public Repo() { } @@ -193,6 +203,9 @@ public class Repo extends ValueObject { case Cols.USER_MIRRORS: userMirrors = Utils.parseCommaSeparatedString(cursor.getString(i)); break; + case Cols.DISABLED_MIRRORS: + disabledMirrors = Utils.parseCommaSeparatedString(cursor.getString(i)); + break; case Cols.PUSH_REQUESTS: pushRequests = cursor.getInt(i); break; @@ -334,6 +347,10 @@ public class Repo extends ValueObject { userMirrors = Utils.parseCommaSeparatedString(values.getAsString(Cols.USER_MIRRORS)); } + if (values.containsKey(Cols.DISABLED_MIRRORS)) { + disabledMirrors = Utils.parseCommaSeparatedString(values.getAsString(Cols.DISABLED_MIRRORS)); + } + if (values.containsKey(Cols.PUSH_REQUESTS)) { pushRequests = toInt(values.getAsInteger(Cols.PUSH_REQUESTS)); } @@ -345,8 +362,9 @@ public class Repo extends ValueObject { * mirror list. */ public boolean hasMirrors() { - return (mirrors != null && mirrors.length > 1) - || (userMirrors != null && userMirrors.length > 0); + List mirrorList = getMirrorList(); + int size = mirrorList.size(); + return size > 1 || (size == 1 && !mirrorList.contains(address)); } /** @@ -361,6 +379,9 @@ public class Repo extends ValueObject { allMirrors.addAll(Arrays.asList(mirrors)); } allMirrors.add(address); + if (disabledMirrors != null) { + allMirrors.removeAll(Arrays.asList(disabledMirrors)); + } return new ArrayList<>(allMirrors); } @@ -382,9 +403,11 @@ public class Repo extends ValueObject { /** * Get a random mirror URL from the list of mirrors for this repo. It will * remove the URL in {@code mirrorToSkip} from consideration before choosing - * which mirror to return. + * which mirror to return. {@link #getMirrorList()} returns a list of all + * known mirrors minus the mirrors that have been disabled by the + * user preference, e.g. {@link #disabledMirrors}. *

- * The mirror logic assumes that it has a mirrors list with at least once + * The mirror logic assumes that it has a mirrors list with at least one * valid entry in it. In the index format as defined by {@code fdroid update}, * there is always at least one valid URL: the canonical URL. That also means * if there is only one item in the mirrors list, there are no other URLs to try. @@ -394,6 +417,8 @@ public class Repo extends ValueObject { * update. That makes it possible to do the first index update via SD Card * or USB OTG drive. * + * @see #getMirrorList() + * @see #disabledMirrors * @see FDroidApp#resetMirrorVars() * @see FDroidApp#switchUrlToNewMirror(String, Repo) * @see FDroidApp#getTimeout() @@ -403,7 +428,7 @@ public class Repo extends ValueObject { mirrorToSkip = address; } List shuffledMirrors = getMirrorList(); - if (shuffledMirrors.size() > 1) { + if (shuffledMirrors.size() > 0) { Collections.shuffle(shuffledMirrors); for (String m : shuffledMirrors) { // Return a non default, and not last used mirror @@ -419,6 +444,6 @@ public class Repo extends ValueObject { } } } - return null; // In case we are out of mirrors. + return address; // In case we are out of mirrors. } } diff --git a/app/src/main/java/org/fdroid/fdroid/data/Schema.java b/app/src/main/java/org/fdroid/fdroid/data/Schema.java index 3642ebcd9..bdb8c01d8 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/Schema.java +++ b/app/src/main/java/org/fdroid/fdroid/data/Schema.java @@ -363,12 +363,13 @@ public interface Schema { String ICON = "icon"; String MIRRORS = "mirrors"; String USER_MIRRORS = "userMirrors"; + String DISABLED_MIRRORS = "disabledMirrors"; String PUSH_REQUESTS = "pushRequests"; String[] ALL = { _ID, ADDRESS, NAME, DESCRIPTION, IN_USE, PRIORITY, SIGNING_CERT, FINGERPRINT, MAX_AGE, LAST_UPDATED, LAST_ETAG, VERSION, IS_SWAP, - USERNAME, PASSWORD, TIMESTAMP, ICON, MIRRORS, USER_MIRRORS, PUSH_REQUESTS, + USERNAME, PASSWORD, TIMESTAMP, ICON, MIRRORS, USER_MIRRORS, DISABLED_MIRRORS, PUSH_REQUESTS, }; } } diff --git a/app/src/main/java/org/fdroid/fdroid/views/RepoDetailsActivity.java b/app/src/main/java/org/fdroid/fdroid/views/RepoDetailsActivity.java index 0f043c35d..10f39964b 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/RepoDetailsActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/RepoDetailsActivity.java @@ -44,6 +44,8 @@ import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.data.Schema.RepoTable; +import java.util.Arrays; +import java.util.HashSet; import java.util.Locale; public class RepoDetailsActivity extends AppCompatActivity { @@ -82,6 +84,8 @@ public class RepoDetailsActivity extends AppCompatActivity { private View repoView; private String shareUrl; + private MirrorAdapter adapterToNotify; + /** * Help function to make switching between two view states easier. * Perhaps there is a better way to do this. I recall that using Adobe @@ -109,25 +113,19 @@ public class RepoDetailsActivity extends AppCompatActivity { repoView = findViewById(R.id.repo_view); repoId = getIntent().getLongExtra(ARG_REPO_ID, 0); - final String[] projection = { - RepoTable.Cols.NAME, - RepoTable.Cols.ADDRESS, - RepoTable.Cols.FINGERPRINT, - RepoTable.Cols.MIRRORS, - RepoTable.Cols.USER_MIRRORS, - }; - repo = RepoProvider.Helper.findById(this, repoId, projection); + repo = RepoProvider.Helper.findById(this, repoId); TextView inputUrl = findViewById(R.id.input_repo_url); inputUrl.setText(repo.address); RecyclerView officialMirrorListView = findViewById(R.id.official_mirror_list); officialMirrorListView.setLayoutManager(new LinearLayoutManager(this)); - officialMirrorListView.setAdapter(new MirrorAdapter(repo.mirrors)); + adapterToNotify = new MirrorAdapter(repo, repo.mirrors); + officialMirrorListView.setAdapter(adapterToNotify); RecyclerView userMirrorListView = findViewById(R.id.user_mirror_list); userMirrorListView.setLayoutManager(new LinearLayoutManager(this)); - userMirrorListView.setAdapter(new MirrorAdapter(repo.userMirrors)); + userMirrorListView.setAdapter(new MirrorAdapter(repo, repo.userMirrors)); if (repo.address.startsWith("content://")) { // no need to show a QR Code, it is not shareable @@ -464,7 +462,8 @@ public class RepoDetailsActivity extends AppCompatActivity { } private class MirrorAdapter extends RecyclerView.Adapter { - private String[] mirrors; + private final Repo repo; + private final String[] mirrors; class MirrorViewHolder extends RecyclerView.ViewHolder { View view; @@ -475,7 +474,8 @@ public class RepoDetailsActivity extends AppCompatActivity { } } - MirrorAdapter(String[] mirrors) { + MirrorAdapter(Repo repo, String[] mirrors) { + this.repo = repo; this.mirrors = mirrors; } @@ -487,12 +487,57 @@ public class RepoDetailsActivity extends AppCompatActivity { } @Override - public void onBindViewHolder(@NonNull MirrorViewHolder holder, int position) { + public void onBindViewHolder(@NonNull MirrorViewHolder holder, final int position) { TextView repoNameTextView = holder.view.findViewById(R.id.repo_name); repoNameTextView.setText(mirrors[position]); + final String itemMirror = mirrors[position]; + boolean enabled = true; + if (repo.disabledMirrors != null) { + for (String disabled : repo.disabledMirrors) { + if (TextUtils.equals(itemMirror, disabled)) { + enabled = false; + break; + } + } + } CompoundButton switchView = holder.view.findViewById(R.id.repo_switch); - switchView.setChecked(true); + switchView.setChecked(enabled); + switchView.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + HashSet 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); + if (disabledMirrors.size() == totalMirrors) { + // if all mirrors are disabled, re-enable canonical repo as mirror + disabledMirrors.remove(repo.address); + 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); + } + }); View repoUnverified = holder.view.findViewById(R.id.repo_unverified); repoUnverified.setVisibility(View.GONE);