From 5e6ff06e0eed9b08673defa926fb13c2da2f7082 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Thu, 29 Mar 2018 20:47:38 +0200 Subject: [PATCH 01/11] bump to build-tools-25.0.3 --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 85bb92ffd..7611374d2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -180,7 +180,7 @@ def preDexEnabled = "true".equals(System.getProperty("pre-dex", "true")) android { compileSdkVersion 24 - buildToolsVersion '25.0.2' + buildToolsVersion '25.0.3' useLibrary 'org.apache.http.legacy' buildTypes { From 34381f9cfb87a86696fa5ad6e28b1c5bed2bd54e Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Wed, 28 Mar 2018 22:53:25 +0200 Subject: [PATCH 02/11] simplify creation of Repo instances in tests Creating a Repo instance first seems totally redundant and confusing. --- .../fdroid/updater/MultiRepoUpdaterTest.java | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/app/src/test/java/org/fdroid/fdroid/updater/MultiRepoUpdaterTest.java b/app/src/test/java/org/fdroid/fdroid/updater/MultiRepoUpdaterTest.java index f7782553d..09026171c 100644 --- a/app/src/test/java/org/fdroid/fdroid/updater/MultiRepoUpdaterTest.java +++ b/app/src/test/java/org/fdroid/fdroid/updater/MultiRepoUpdaterTest.java @@ -158,22 +158,17 @@ public abstract class MultiRepoUpdaterTest extends FDroidProviderTest { return createRepo(name, uri, context, PUB_KEY); } + /** + * Creates a real instance of {@code Repo} by loading it from the database, + * that ensures it includes the primary key from the database. + */ static Repo createRepo(String name, String uri, Context context, String signingCert) { - Repo repo = new Repo(); - repo.signingCertificate = signingCert; - repo.address = uri; - repo.name = name; - ContentValues values = new ContentValues(3); - values.put(Schema.RepoTable.Cols.SIGNING_CERT, repo.signingCertificate); - values.put(Schema.RepoTable.Cols.ADDRESS, repo.address); - values.put(Schema.RepoTable.Cols.NAME, repo.name); - + values.put(Schema.RepoTable.Cols.SIGNING_CERT, signingCert); + values.put(Schema.RepoTable.Cols.ADDRESS, uri); + values.put(Schema.RepoTable.Cols.NAME, name); RepoProvider.Helper.insert(context, values); - - // Need to reload the repo based on address so that it includes the primary key from - // the database. - return RepoProvider.Helper.findByAddress(context, repo.address); + return RepoProvider.Helper.findByAddress(context, uri); } protected RepoUpdater createRepoUpdater(String name, String uri, Context context) { From dd481035163b763525187766301772e6e7aaa608 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Wed, 28 Mar 2018 23:08:59 +0200 Subject: [PATCH 03/11] purge unused repo instance variable from UpdateException The repo instance variable has long since been unused, but has just been left there as a vestige. Now its presence is blocking RepoUpdater. getSigningCertFromJar() from being a static method that can be reused when checking for repos on SD Cards and other removable storage devices. --- .../org/fdroid/fdroid/IndexV1Updater.java | 16 ++++----- .../java/org/fdroid/fdroid/RepoUpdater.java | 33 +++++++++---------- .../org/fdroid/fdroid/data/RepoPersister.java | 4 +-- 3 files changed, 24 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/IndexV1Updater.java b/app/src/main/java/org/fdroid/fdroid/IndexV1Updater.java index fd0f1563b..b53a6a17e 100644 --- a/app/src/main/java/org/fdroid/fdroid/IndexV1Updater.java +++ b/app/src/main/java/org/fdroid/fdroid/IndexV1Updater.java @@ -135,7 +135,7 @@ public class IndexV1Updater extends RepoUpdater { if (downloader != null) { FileUtils.deleteQuietly(downloader.outputFile); } - throw new RepoUpdater.UpdateException(repo, "Error getting index file", e2); + throw new RepoUpdater.UpdateException("Error getting index file", e2); } catch (InterruptedException e2) { // ignored if canceled, the local database just won't be updated } @@ -144,7 +144,7 @@ public class IndexV1Updater extends RepoUpdater { if (downloader != null) { FileUtils.deleteQuietly(downloader.outputFile); } - throw new RepoUpdater.UpdateException(repo, "Error getting index file", e); + throw new RepoUpdater.UpdateException("Error getting index file", e); } catch (InterruptedException e) { // ignored if canceled, the local database just won't be updated } @@ -236,7 +236,7 @@ public class IndexV1Updater extends RepoUpdater { long timestamp = (Long) repoMap.get("timestamp") / 1000; if (repo.timestamp > timestamp) { - throw new RepoUpdater.UpdateException(repo, "index.jar is older that current index! " + throw new RepoUpdater.UpdateException("index.jar is older that current index! " + timestamp + " < " + repo.timestamp); } @@ -410,16 +410,14 @@ public class IndexV1Updater extends RepoUpdater { String certFromJar = Hasher.hex(rawCertFromJar); if (TextUtils.isEmpty(certFromJar)) { - throw new SigningException(repo, - SIGNED_FILE_NAME + " must have an included signing certificate!"); + throw new SigningException(SIGNED_FILE_NAME + " must have an included signing certificate!"); } if (repo.signingCertificate == null) { if (repo.fingerprint != null) { String fingerprintFromJar = Utils.calcFingerprint(rawCertFromJar); if (!repo.fingerprint.equalsIgnoreCase(fingerprintFromJar)) { - throw new SigningException(repo, - "Supplied certificate fingerprint does not match!"); + throw new SigningException("Supplied certificate fingerprint does not match!"); } } Utils.debugLog(TAG, "Saving new signing certificate to database for " + repo.address); @@ -431,14 +429,14 @@ public class IndexV1Updater extends RepoUpdater { } if (TextUtils.isEmpty(repo.signingCertificate)) { - throw new SigningException(repo, "A empty repo signing certificate is invalid!"); + throw new SigningException("A empty repo signing certificate is invalid!"); } if (repo.signingCertificate.equals(certFromJar)) { return; // we have a match! } - throw new SigningException(repo, "Signing certificate does not match!"); + throw new SigningException("Signing certificate does not match!"); } } diff --git a/app/src/main/java/org/fdroid/fdroid/RepoUpdater.java b/app/src/main/java/org/fdroid/fdroid/RepoUpdater.java index 2ac6e7da9..9c3463b7f 100644 --- a/app/src/main/java/org/fdroid/fdroid/RepoUpdater.java +++ b/app/src/main/java/org/fdroid/fdroid/RepoUpdater.java @@ -139,7 +139,7 @@ public class RepoUpdater { } } - throw new UpdateException(repo, "Error getting index file", e); + throw new UpdateException("Error getting index file", e); } catch (InterruptedException e) { // ignored if canceled, the local database just won't be updated e.printStackTrace(); @@ -202,7 +202,7 @@ public class RepoUpdater { InputStream indexInputStream = null; try { if (downloadedFile == null || !downloadedFile.exists()) { - throw new UpdateException(repo, downloadedFile + " does not exist!"); + throw new UpdateException(downloadedFile + " does not exist!"); } // Due to a bug in Android 5.0 Lollipop, the inclusion of spongycastle causes @@ -226,7 +226,7 @@ public class RepoUpdater { long timestamp = repoDetailsToSave.getAsLong(RepoTable.Cols.TIMESTAMP); if (timestamp < repo.timestamp) { - throw new UpdateException(repo, "index.jar is older that current index! " + throw new UpdateException("index.jar is older that current index! " + timestamp + " < " + repo.timestamp); } @@ -237,7 +237,7 @@ public class RepoUpdater { assertSigningCertFromXmlCorrect(); commitToDb(); } catch (SAXException | ParserConfigurationException | IOException e) { - throw new UpdateException(repo, "Error parsing index", e); + throw new UpdateException("Error parsing index", e); } finally { FDroidApp.enableSpongyCastleOnLollipop(); Utils.closeQuietly(indexInputStream); @@ -343,22 +343,19 @@ public class RepoUpdater { public static class UpdateException extends Exception { private static final long serialVersionUID = -4492452418826132803L; - public final Repo repo; - public UpdateException(Repo repo, String message) { + public UpdateException(String message) { super(message); - this.repo = repo; } - public UpdateException(Repo repo, String message, Exception cause) { + public UpdateException(String message, Exception cause) { super(message, cause); - this.repo = repo; } } public static class SigningException extends UpdateException { - public SigningException(Repo repo, String message) { - super(repo, "Repository was not signed correctly: " + message); + public SigningException(String message) { + super("Repository was not signed correctly: " + message); } } @@ -367,18 +364,18 @@ public class RepoUpdater { * signing setups that would be valid for a regular jar. This validates those * restrictions. */ - X509Certificate getSigningCertFromJar(JarEntry jarEntry) throws SigningException { + public static X509Certificate getSigningCertFromJar(JarEntry jarEntry) throws SigningException { final CodeSigner[] codeSigners = jarEntry.getCodeSigners(); if (codeSigners == null || codeSigners.length == 0) { - throw new SigningException(repo, "No signature found in index"); + throw new SigningException("No signature found in index"); } /* we could in theory support more than 1, but as of now we do not */ if (codeSigners.length > 1) { - throw new SigningException(repo, "index.jar must be signed by a single code signer!"); + throw new SigningException("index.jar must be signed by a single code signer!"); } List certs = codeSigners[0].getSignerCertPath().getCertificates(); if (certs.size() != 1) { - throw new SigningException(repo, "index.jar code signers must only have a single certificate!"); + throw new SigningException("index.jar code signers must only have a single certificate!"); } return (X509Certificate) certs.get(0); } @@ -404,7 +401,7 @@ public class RepoUpdater { String fingerprintFromJar = Utils.calcFingerprint(rawCertFromJar); if (!repo.fingerprint.equalsIgnoreCase(fingerprintFromIndexXml) || !repo.fingerprint.equalsIgnoreCase(fingerprintFromJar)) { - throw new SigningException(repo, "Supplied certificate fingerprint does not match!"); + throw new SigningException("Supplied certificate fingerprint does not match!"); } } // else - no info to check things are valid, so just Trust On First Use @@ -435,7 +432,7 @@ public class RepoUpdater { if (TextUtils.isEmpty(repo.signingCertificate) || TextUtils.isEmpty(certFromJar) || TextUtils.isEmpty(certFromIndexXml)) { - throw new SigningException(repo, "A empty repo or signing certificate is invalid!"); + throw new SigningException("A empty repo or signing certificate is invalid!"); } // though its called repo.signingCertificate, its actually a X509 certificate @@ -444,7 +441,7 @@ public class RepoUpdater { && certFromIndexXml.equals(certFromJar)) { return; // we have a match! } - throw new SigningException(repo, "Signing certificate does not match!"); + throw new SigningException("Signing certificate does not match!"); } /** diff --git a/app/src/main/java/org/fdroid/fdroid/data/RepoPersister.java b/app/src/main/java/org/fdroid/fdroid/data/RepoPersister.java index f278de932..d8052b075 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/RepoPersister.java +++ b/app/src/main/java/org/fdroid/fdroid/data/RepoPersister.java @@ -106,7 +106,7 @@ public class RepoPersister { try { context.getContentResolver().applyBatch(TempApkProvider.getAuthority(), apkOperations); } catch (RemoteException | OperationApplicationException e) { - throw new RepoUpdater.UpdateException(repo, "An internal error occurred while updating the database", e); + throw new RepoUpdater.UpdateException("An internal error occurred while updating the database", e); } } @@ -122,7 +122,7 @@ public class RepoPersister { context.getContentResolver().applyBatch(TempAppProvider.getAuthority(), appOperations); return getIdsForPackages(appsToSave); } catch (RemoteException | OperationApplicationException e) { - throw new RepoUpdater.UpdateException(repo, "An internal error occurred while updating the database", e); + throw new RepoUpdater.UpdateException("An internal error occurred while updating the database", e); } } From b3d90cd1b6d16d791c4b10a5261aab055e767737 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Wed, 28 Mar 2018 23:56:19 +0200 Subject: [PATCH 04/11] allow any path in incoming add repo Intent, filters check the paths If anything wants to craft an Intent to send directly to F-Droid with an arbitrary but valid path, that seems like a fine thing to support. The IntentFilters will still only match on the well known paths, so that the user doesn't see F-Droid claiming all HTTP URLs. --- .../org/fdroid/fdroid/data/NewRepoConfig.java | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/data/NewRepoConfig.java b/app/src/main/java/org/fdroid/fdroid/data/NewRepoConfig.java index c1242f345..8bf572b2c 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/NewRepoConfig.java +++ b/app/src/main/java/org/fdroid/fdroid/data/NewRepoConfig.java @@ -4,7 +4,6 @@ import android.content.Context; import android.content.Intent; import android.net.Uri; import android.text.TextUtils; - import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.localrepo.peers.WifiPeer; @@ -82,17 +81,12 @@ public class NewRepoConfig { scheme = scheme.toLowerCase(Locale.ENGLISH); host = host.toLowerCase(Locale.ENGLISH); - // We only listen for /fdroid/archive or /fdroid/repo paths when receiving a HTTP(S) intent. - // For fdroidrepo(s) intents, we are less picky and will accept any path. - boolean isHttpScheme = TextUtils.equals("http", scheme) || TextUtils.equals("https", scheme); - String path = uri.getPath(); - if (path == null || isHttpScheme && !(path.contains("/fdroid/archive") || path.contains("/fdroid/repo"))) { + if (uri.getPath() == null + || !Arrays.asList("https", "http", "fdroidrepos", "fdroidrepo").contains(scheme)) { isValidRepo = false; return; } - boolean isFdroidScheme = TextUtils.equals("fdroidrepo", scheme) || TextUtils.equals("fdroidrepos", scheme); - String userInfo = uri.getUserInfo(); if (userInfo != null) { String[] userInfoTokens = userInfo.split(":"); @@ -109,15 +103,8 @@ public class NewRepoConfig { bssid = uri.getQueryParameter("bssid"); ssid = uri.getQueryParameter("ssid"); fromSwap = uri.getQueryParameter("swap") != null; - - if (!isFdroidScheme && !isHttpScheme) { - isValidRepo = false; - return; - } - uriString = sanitizeRepoUri(uri); isValidRepo = true; - } public String getBssid() { @@ -175,7 +162,9 @@ public class NewRepoConfig { return errorMessage; } - /** Sanitize and format an incoming repo URI for function and readability */ + /** + * Sanitize and format an incoming repo URI for function and readability + */ public static String sanitizeRepoUri(Uri uri) { String scheme = uri.getScheme(); String host = uri.getHost(); From 759c3b90fc76474b97b3fc5f092615a3ec53c864 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Thu, 29 Mar 2018 14:16:25 +0200 Subject: [PATCH 05/11] display mirrors in RepoDetailsActivity This is rough, but better than nothing. --- .../fdroid/views/RepoDetailsActivity.java | 68 ++++++++++++------- app/src/main/res/layout/repodetails.xml | 24 +++++++ app/src/main/res/values/strings.xml | 2 + 3 files changed, 70 insertions(+), 24 deletions(-) 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 d12623aa1..2962ed863 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/RepoDetailsActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/RepoDetailsActivity.java @@ -50,26 +50,26 @@ public class RepoDetailsActivity extends ActionBarActivity { * all of this info, otherwise they will be hidden. */ private static final int[] SHOW_IF_EXISTS = { - R.id.label_repo_name, - R.id.text_repo_name, - R.id.text_description, - R.id.label_num_apps, - R.id.text_num_apps, - R.id.label_last_update, - R.id.text_last_update, - R.id.label_username, - R.id.text_username, - R.id.button_edit_credentials, - R.id.label_repo_fingerprint, - R.id.text_repo_fingerprint, - R.id.text_repo_fingerprint_description, + R.id.label_repo_name, + R.id.text_repo_name, + R.id.text_description, + R.id.label_num_apps, + R.id.text_num_apps, + R.id.label_last_update, + R.id.text_last_update, + R.id.label_username, + R.id.text_username, + R.id.button_edit_credentials, + R.id.label_repo_fingerprint, + R.id.text_repo_fingerprint, + R.id.text_repo_fingerprint_description, }; /** * If the repo has not been updated yet, then we only show * these, otherwise they are hidden. */ private static final int[] HIDE_IF_EXISTS = { - R.id.text_not_yet_updated, + R.id.text_not_yet_updated, }; private Repo repo; private long repoId; @@ -108,12 +108,18 @@ public class RepoDetailsActivity extends ActionBarActivity { RepoTable.Cols.NAME, RepoTable.Cols.ADDRESS, RepoTable.Cols.FINGERPRINT, + RepoTable.Cols.MIRRORS, }; repo = RepoProvider.Helper.findById(this, repoId, projection); TextView inputUrl = (TextView) findViewById(R.id.input_repo_url); inputUrl.setText(repo.address); + if (repo.address.startsWith("content://")) { + // 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(); String qrUriString = uri.toString(); @@ -321,6 +327,20 @@ public class RepoDetailsActivity extends ActionBarActivity { TextView numApps = (TextView) repoView.findViewById(R.id.text_num_apps); TextView lastUpdated = (TextView) repoView.findViewById(R.id.text_last_update); + if (repo.mirrors != null) { + TextView officialMirrorsLabel = (TextView) repoView.findViewById(R.id.label_official_mirrors); + officialMirrorsLabel.setVisibility(View.VISIBLE); + TextView officialMirrorsText = (TextView) repoView.findViewById(R.id.text_official_mirrors); + officialMirrorsText.setVisibility(View.VISIBLE); + StringBuilder builder = new StringBuilder(); + for (String url : repo.mirrors) { + builder.append("◦ "); + builder.append(url); + builder.append('\n'); + } + officialMirrorsText.setText(builder.toString()); + } + name.setText(repo.name); int appCount = RepoProvider.Helper.countAppsForRepo(this, repoId); @@ -345,22 +365,22 @@ public class RepoDetailsActivity extends ActionBarActivity { private void promptForDelete() { new AlertDialog.Builder(this) - .setTitle(R.string.repo_confirm_delete_title) - .setMessage(R.string.repo_confirm_delete_body) - .setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - RepoProvider.Helper.remove(getApplicationContext(), repoId); - finish(); - } - }).setNegativeButton(android.R.string.cancel, + .setTitle(R.string.repo_confirm_delete_title) + .setMessage(R.string.repo_confirm_delete_body) + .setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + RepoProvider.Helper.remove(getApplicationContext(), repoId); + finish(); + } + }).setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // Do nothing... } } - ).show(); + ).show(); } public void showChangePasswordDialog(final View parentView) { diff --git a/app/src/main/res/layout/repodetails.xml b/app/src/main/res/layout/repodetails.xml index 8c3711c81..375db46ca 100644 --- a/app/src/main/res/layout/repodetails.xml +++ b/app/src/main/res/layout/repodetails.xml @@ -71,6 +71,30 @@ android:id="@+id/text_last_update" style="@style/BodyText" /> + + + + + + + + Fingerprint of the signing key (SHA-256) Description Last update + Official mirrors + User mirrors Name This means that the list of apps could not be verified. You should be careful From 89e04cc078bb96a6999ccc4a42f23800272e2be2 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Thu, 29 Mar 2018 14:48:21 +0200 Subject: [PATCH 06/11] include repo name in messages in "App Repo" dialog This should hopefully make things a little clearer to the user. --- .../fdroid/views/ManageReposActivity.java | 54 +++++++++++-------- app/src/main/res/values/strings.xml | 10 ++-- 2 files changed, 35 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java b/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java index bc470667f..9be324d88 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java @@ -275,7 +275,7 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana try { url = normalizeUrl(url); } catch (URISyntaxException e) { - invalidUrl(); + invalidUrl(null); return; } @@ -364,69 +364,77 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana final Repo repo = !TextUtils.isEmpty(uri) ? RepoProvider.Helper.findByAddress(context, uri) : null; if (repo == null) { - repoDoesntExist(); + repoDoesntExist(repo); } else { if (repo.isSwap) { - repoIsSwap(); + repoIsSwap(repo); } else if (repo.fingerprint == null && fingerprint.length() > 0) { - upgradingToSigned(); + upgradingToSigned(repo); } else if (repo.fingerprint != null && !repo.fingerprint.equalsIgnoreCase(fingerprint)) { - repoFingerprintDoesntMatch(); + repoFingerprintDoesntMatch(repo); } else { // Could be either an unsigned repo, and no fingerprint was supplied, // or it could be a signed repo with a matching fingerprint. if (repo.inuse) { - repoExistsAndEnabled(); + repoExistsAndEnabled(repo); } else { - repoExistsAndDisabled(); + repoExistsAndDisabled(repo); } } } } - private void repoDoesntExist() { - updateUi(AddRepoState.DOESNT_EXIST, 0, false, R.string.repo_add_add, true); + private void repoDoesntExist(Repo repo) { + updateUi(repo, AddRepoState.DOESNT_EXIST, 0, false, R.string.repo_add_add, true); } - private void repoIsSwap() { - updateUi(AddRepoState.IS_SWAP, 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() { - updateUi(AddRepoState.EXISTS_FINGERPRINT_MISMATCH, R.string.repo_delete_to_overwrite, + private void repoFingerprintDoesntMatch(Repo repo) { + updateUi(repo, AddRepoState.EXISTS_FINGERPRINT_MISMATCH, + R.string.repo_delete_to_overwrite, true, R.string.overwrite, false); } - private void invalidUrl() { - updateUi(AddRepoState.INVALID_URL, R.string.invalid_url, true, + private void invalidUrl(Repo repo) { + updateUi(repo, AddRepoState.INVALID_URL, R.string.invalid_url, true, R.string.repo_add_add, false); } - private void repoExistsAndDisabled() { - updateUi(AddRepoState.EXISTS_DISABLED, + private void repoExistsAndDisabled(Repo repo) { + updateUi(repo, AddRepoState.EXISTS_DISABLED, R.string.repo_exists_enable, false, R.string.enable, true); } - private void repoExistsAndEnabled() { - updateUi(AddRepoState.EXISTS_ENABLED, R.string.repo_exists_and_enabled, false, + private void repoExistsAndEnabled(Repo repo) { + updateUi(repo, AddRepoState.EXISTS_ENABLED, R.string.repo_exists_and_enabled, false, R.string.ok, true); } - private void upgradingToSigned() { - updateUi(AddRepoState.EXISTS_UPGRADABLE_TO_SIGNED, R.string.repo_exists_add_fingerprint, + private void repoExistsAddMirror(Repo repo) { + updateUi(repo, AddRepoState.EXISTS_ADD_MIRROR, R.string.repo_exists_add_mirror, false, + R.string.repo_add_mirror, true); + } + + private void upgradingToSigned(Repo repo) { + updateUi(repo, AddRepoState.EXISTS_UPGRADABLE_TO_SIGNED, R.string.repo_exists_add_fingerprint, false, R.string.add_key, true); } - private void updateUi(AddRepoState state, int messageRes, boolean redMessage, int addTextRes, boolean addEnabled) { + @DebugLog + private void updateUi(Repo repo, AddRepoState state, int messageRes, boolean redMessage, int addTextRes, + boolean addEnabled) { if (addRepoState != state) { addRepoState = state; if (messageRes > 0) { - overwriteMessage.setText(messageRes); + overwriteMessage.setText(String.format(getString(messageRes), repo.name)); overwriteMessage.setVisibility(View.VISIBLE); if (redMessage) { overwriteMessage.setTextColor(getResources().getColor(R.color.red)); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a6c8f96dc..855087c95 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -134,12 +134,10 @@ This often occurs with apps installed via Google Play or other sources, if they Repository address Fingerprint (optional) - This repo is already setup, this will add new key information. - This repo is already setup, confirm that you want to re-enable it. - The incoming repo is already setup and enabled. - You must first delete this repo before you can add one with a different - key. - + %1$s is already setup, this will add new key information. + %1$s is already setup, confirm that you want to re-enable it. + %1$s is already setup and enabled. + First delete %1$s in order to add this with a conflicting key. Bad fingerprint This is not a valid URL. Ignoring malformed repo URI: %s From 2f0cb30ad001ae15850acb54fca475264d368b6e Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Thu, 29 Mar 2018 14:53:38 +0200 Subject: [PATCH 07/11] support adding custom mirrors to any existing repo, via "App Repo" This lets people add any URL as a mirror to an existing repo. The UX is people add URLs via any of the normal ways of adding a new repo via Intents, like clicking URLs, QRCodes, etc. --- .../java/org/fdroid/fdroid/data/DBHelper.java | 100 ++++++++++-------- .../java/org/fdroid/fdroid/data/Repo.java | 47 ++++++-- .../java/org/fdroid/fdroid/data/Schema.java | 3 +- .../fdroid/views/ManageReposActivity.java | 87 +++++++++++++-- .../fdroid/views/RepoDetailsActivity.java | 14 +++ app/src/main/res/values/strings.xml | 2 + 6 files changed, 188 insertions(+), 65 deletions(-) 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 45a936473..e0534384f 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java @@ -88,35 +88,36 @@ public class DBHelper extends SQLiteOpenHelper { + RepoTable.Cols.TIMESTAMP + " integer not null default 0, " + RepoTable.Cols.ICON + " string, " + RepoTable.Cols.MIRRORS + " string, " + + RepoTable.Cols.USER_MIRRORS + " string, " + RepoTable.Cols.PUSH_REQUESTS + " integer not null default " + Repo.PUSH_REQUEST_IGNORE + ");"; static final String CREATE_TABLE_APK = "CREATE TABLE " + ApkTable.NAME + " ( " - + ApkTable.Cols.APP_ID + " integer not null, " - + ApkTable.Cols.VERSION_NAME + " text not null, " - + ApkTable.Cols.REPO_ID + " integer not null, " - + ApkTable.Cols.HASH + " text not null, " - + ApkTable.Cols.VERSION_CODE + " int not null," - + ApkTable.Cols.NAME + " text not null, " - + ApkTable.Cols.SIZE + " int not null, " - + ApkTable.Cols.SIGNATURE + " string, " - + ApkTable.Cols.SOURCE_NAME + " string, " - + ApkTable.Cols.MIN_SDK_VERSION + " integer, " - + ApkTable.Cols.TARGET_SDK_VERSION + " integer, " - + ApkTable.Cols.MAX_SDK_VERSION + " integer, " - + ApkTable.Cols.OBB_MAIN_FILE + " string, " - + ApkTable.Cols.OBB_MAIN_FILE_SHA256 + " string, " - + ApkTable.Cols.OBB_PATCH_FILE + " string, " - + ApkTable.Cols.OBB_PATCH_FILE_SHA256 + " string, " - + ApkTable.Cols.REQUESTED_PERMISSIONS + " string, " - + ApkTable.Cols.FEATURES + " string, " - + ApkTable.Cols.NATIVE_CODE + " string, " - + ApkTable.Cols.HASH_TYPE + " string, " - + ApkTable.Cols.ADDED_DATE + " string, " - + ApkTable.Cols.IS_COMPATIBLE + " int not null, " - + ApkTable.Cols.INCOMPATIBLE_REASONS + " text" - + ");"; + + ApkTable.Cols.APP_ID + " integer not null, " + + ApkTable.Cols.VERSION_NAME + " text not null, " + + ApkTable.Cols.REPO_ID + " integer not null, " + + ApkTable.Cols.HASH + " text not null, " + + ApkTable.Cols.VERSION_CODE + " int not null," + + ApkTable.Cols.NAME + " text not null, " + + ApkTable.Cols.SIZE + " int not null, " + + ApkTable.Cols.SIGNATURE + " string, " + + ApkTable.Cols.SOURCE_NAME + " string, " + + ApkTable.Cols.MIN_SDK_VERSION + " integer, " + + ApkTable.Cols.TARGET_SDK_VERSION + " integer, " + + ApkTable.Cols.MAX_SDK_VERSION + " integer, " + + ApkTable.Cols.OBB_MAIN_FILE + " string, " + + ApkTable.Cols.OBB_MAIN_FILE_SHA256 + " string, " + + ApkTable.Cols.OBB_PATCH_FILE + " string, " + + ApkTable.Cols.OBB_PATCH_FILE_SHA256 + " string, " + + ApkTable.Cols.REQUESTED_PERMISSIONS + " string, " + + ApkTable.Cols.FEATURES + " string, " + + ApkTable.Cols.NATIVE_CODE + " string, " + + ApkTable.Cols.HASH_TYPE + " string, " + + ApkTable.Cols.ADDED_DATE + " string, " + + ApkTable.Cols.IS_COMPATIBLE + " int not null, " + + ApkTable.Cols.INCOMPATIBLE_REASONS + " text" + + ");"; static final String CREATE_TABLE_APP_METADATA = "CREATE TABLE " + AppMetadataTable.NAME + " ( " @@ -181,7 +182,7 @@ public class DBHelper extends SQLiteOpenHelper { * app metadata id, because it can instead look through the primary key index. This can be * observed by flipping the order of the primary key columns, and noting the resulting sqlite * logs along the lines of: - * E/SQLiteLog(14164): (284) automatic index on fdroid_categoryAppMetadataJoin(appMetadataId) + * E/SQLiteLog(14164): (284) automatic index on fdroid_categoryAppMetadataJoin(appMetadataId) */ static final String CREATE_TABLE_CAT_JOIN = "CREATE TABLE " + CatJoinTable.NAME + " ( " @@ -214,7 +215,7 @@ public class DBHelper extends SQLiteOpenHelper { + "primary key(" + ApkAntiFeatureJoinTable.Cols.APK_ID + ", " + ApkAntiFeatureJoinTable.Cols.ANTI_FEATURE_ID + ") " + " );"; - protected static final int DB_VERSION = 77; + protected static final int DB_VERSION = 78; private final Context context; @@ -321,6 +322,17 @@ public class DBHelper extends SQLiteOpenHelper { addApkAntiFeatures(db, oldVersion); addIgnoreVulnPref(db, oldVersion); addLiberapayID(db, oldVersion); + addUserMirrorsFields(db, oldVersion); + } + + private void addUserMirrorsFields(SQLiteDatabase db, int oldVersion) { + if (oldVersion >= 78) { + return; + } + if (!columnExists(db, RepoTable.NAME, RepoTable.Cols.USER_MIRRORS)) { + Utils.debugLog(TAG, "Adding " + RepoTable.Cols.USER_MIRRORS + " field to " + RepoTable.NAME + " table in db."); + db.execSQL("alter table " + RepoTable.NAME + " add column " + RepoTable.Cols.USER_MIRRORS + " string;"); + } } private void addLiberapayID(SQLiteDatabase db, int oldVersion) { @@ -581,7 +593,7 @@ public class DBHelper extends SQLiteOpenHelper { updateRepoPriority(db, gpPubKey, gpArchiveAddress, 4); int priority = 5; - String[] projection = new String[] {RepoTable.Cols.SIGNING_CERT, RepoTable.Cols.ADDRESS}; + String[] projection = new String[]{RepoTable.Cols.SIGNING_CERT, RepoTable.Cols.ADDRESS}; // Order by ID, because that is a good analogy for the order in which they were added. // The order in which they were added is likely the order they present in the ManageRepos activity. @@ -606,7 +618,7 @@ public class DBHelper extends SQLiteOpenHelper { RepoTable.NAME, values, RepoTable.Cols.SIGNING_CERT + " = ? AND " + RepoTable.Cols.ADDRESS + " = ?", - new String[] {signingCert, address} + new String[]{signingCert, address} ); } @@ -630,15 +642,15 @@ public class DBHelper extends SQLiteOpenHelper { Utils.debugLog(TAG, "Migrating app preferences to separate table"); db.execSQL( "INSERT INTO " + AppPrefsTable.NAME + " (" - + AppPrefsTable.Cols.PACKAGE_NAME + ", " - + AppPrefsTable.Cols.IGNORE_THIS_UPDATE + ", " - + AppPrefsTable.Cols.IGNORE_ALL_UPDATES - + ") SELECT " - + "id, " - + "ignoreThisUpdate, " - + "ignoreAllUpdates " - + "FROM " + AppMetadataTable.NAME + " " - + "WHERE ignoreThisUpdate > 0 OR ignoreAllUpdates > 0" + + AppPrefsTable.Cols.PACKAGE_NAME + ", " + + AppPrefsTable.Cols.IGNORE_THIS_UPDATE + ", " + + AppPrefsTable.Cols.IGNORE_ALL_UPDATES + + ") SELECT " + + "id, " + + "ignoreThisUpdate, " + + "ignoreAllUpdates " + + "FROM " + AppMetadataTable.NAME + " " + + "WHERE ignoreThisUpdate > 0 OR ignoreAllUpdates > 0" ); resetTransient(db); @@ -687,7 +699,7 @@ public class DBHelper extends SQLiteOpenHelper { db.execSQL(createTableDdl); - String nonPackageNameFields = TextUtils.join(", ", new String[] { + String nonPackageNameFields = TextUtils.join(", ", new String[]{ ApkTable.Cols.APP_ID, ApkTable.Cols.VERSION_NAME, ApkTable.Cols.REPO_ID, @@ -766,7 +778,7 @@ public class DBHelper extends SQLiteOpenHelper { } List oldrepos = new ArrayList<>(); Cursor cursor = db.query(RepoTable.NAME, - new String[] {RepoTable.Cols.ADDRESS, RepoTable.Cols.IN_USE, RepoTable.Cols.SIGNING_CERT}, + new String[]{RepoTable.Cols.ADDRESS, RepoTable.Cols.IN_USE, RepoTable.Cols.SIGNING_CERT}, null, null, null, null, null); if (cursor != null) { if (cursor.getCount() > 0) { @@ -847,7 +859,7 @@ public class DBHelper extends SQLiteOpenHelper { } List oldrepos = new ArrayList<>(); Cursor cursor = db.query(RepoTable.NAME, - new String[] {RepoTable.Cols.ADDRESS, RepoTable.Cols.SIGNING_CERT}, + new String[]{RepoTable.Cols.ADDRESS, RepoTable.Cols.SIGNING_CERT}, null, null, null, null, null); if (cursor != null) { if (cursor.getCount() > 0) { @@ -865,7 +877,7 @@ public class DBHelper extends SQLiteOpenHelper { for (final Repo repo : oldrepos) { ContentValues values = new ContentValues(); values.put(RepoTable.Cols.FINGERPRINT, Utils.calcFingerprint(repo.signingCertificate)); - db.update(RepoTable.NAME, values, RepoTable.Cols.ADDRESS + " = ?", new String[] {repo.address}); + db.update(RepoTable.NAME, values, RepoTable.Cols.ADDRESS + " = ?", new String[]{repo.address}); } } @@ -954,7 +966,7 @@ public class DBHelper extends SQLiteOpenHelper { db.execSQL(createTableDdl); - String nonIdFields = TextUtils.join(", ", new String[] { + String nonIdFields = TextUtils.join(", ", new String[]{ RepoTable.Cols.ADDRESS, RepoTable.Cols.NAME, RepoTable.Cols.DESCRIPTION, @@ -1235,8 +1247,8 @@ public class DBHelper extends SQLiteOpenHelper { } private static boolean tableExists(SQLiteDatabase db, String table) { - Cursor cursor = db.query("sqlite_master", new String[] {"name"}, - "type = 'table' AND name = ?", new String[] {table}, null, null, null); + Cursor cursor = db.query("sqlite_master", new String[]{"name"}, + "type = 'table' AND name = ?", new String[]{table}, null, null, null); boolean exists = cursor.getCount() > 0; cursor.close(); 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 eafc06d78..348b19be6 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/Repo.java +++ b/app/src/main/java/org/fdroid/fdroid/data/Repo.java @@ -26,13 +26,13 @@ package org.fdroid.fdroid.data; import android.content.ContentValues; import android.database.Cursor; import android.text.TextUtils; - import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.Schema.RepoTable.Cols; import java.net.MalformedURLException; import java.net.URL; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; @@ -94,6 +94,11 @@ public class Repo extends ValueObject { /** Official mirrors of this repo, considered automatically interchangeable */ public String[] mirrors; + /** + * Mirrors added by the user, either by UI input or by attaching removeable storage + */ + public String[] userMirrors; + /** How to treat push requests included in this repo's index XML */ public int pushRequests = PUSH_REQUEST_IGNORE; @@ -160,6 +165,9 @@ public class Repo extends ValueObject { case Cols.MIRRORS: mirrors = Utils.parseCommaSeparatedString(cursor.getString(i)); break; + case Cols.USER_MIRRORS: + userMirrors = Utils.parseCommaSeparatedString(cursor.getString(i)); + break; case Cols.PUSH_REQUESTS: pushRequests = cursor.getInt(i); break; @@ -297,26 +305,43 @@ public class Repo extends ValueObject { mirrors = Utils.parseCommaSeparatedString(values.getAsString(Cols.MIRRORS)); } + if (values.containsKey(Cols.USER_MIRRORS)) { + userMirrors = Utils.parseCommaSeparatedString(values.getAsString(Cols.USER_MIRRORS)); + } + if (values.containsKey(Cols.PUSH_REQUESTS)) { pushRequests = toInt(values.getAsInteger(Cols.PUSH_REQUESTS)); } } public boolean hasMirrors() { - return mirrors != null && mirrors.length > 1; + return (mirrors != null && mirrors.length > 1) + || (userMirrors != null && userMirrors.length > 0); } + public List getMirrorList() { + final ArrayList allMirrors = new ArrayList(); + if (userMirrors != null) { + allMirrors.addAll(Arrays.asList(userMirrors)); + } + if (mirrors != null) { + allMirrors.addAll(Arrays.asList(mirrors)); + } + return allMirrors; + } + + /** + * Get the number of available mirrors, including the canonical repo. + */ public int getMirrorCount() { int count = 0; - if (mirrors != null && mirrors.length > 1) { - for (String m: mirrors) { - if (!m.equals(address)) { - if (FDroidApp.isUsingTor()) { + for (String m : getMirrorList()) { + if (!m.equals(address)) { + if (FDroidApp.isUsingTor()) { + count++; + } else { + if (!m.contains(".onion")) { count++; - } else { - if (!m.contains(".onion")) { - count++; - } } } } @@ -328,7 +353,7 @@ public class Repo extends ValueObject { if (TextUtils.isEmpty(lastWorkingMirror)) { lastWorkingMirror = address; } - List shuffledMirrors = Arrays.asList(mirrors); + List shuffledMirrors = getMirrorList(); Collections.shuffle(shuffledMirrors); if (shuffledMirrors.size() > 1) { for (String m : shuffledMirrors) { 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 9bcd11103..3642ebcd9 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/Schema.java +++ b/app/src/main/java/org/fdroid/fdroid/data/Schema.java @@ -362,12 +362,13 @@ public interface Schema { String TIMESTAMP = "timestamp"; String ICON = "icon"; String MIRRORS = "mirrors"; + String USER_MIRRORS = "userMirrors"; 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, PUSH_REQUESTS, + USERNAME, PASSWORD, TIMESTAMP, ICON, MIRRORS, USER_MIRRORS, PUSH_REQUESTS, }; } } diff --git a/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java b/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java index 9be324d88..e552b639a 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java @@ -61,12 +61,15 @@ 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; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.util.Arrays; +import java.util.HashMap; import java.util.Locale; @SuppressWarnings("LineLength") @@ -76,7 +79,7 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana private static final String DEFAULT_NEW_REPO_TEXT = "https://"; private enum AddRepoState { - DOESNT_EXIST, EXISTS_FINGERPRINT_MISMATCH, EXISTS_FINGERPRINT_MATCH, + DOESNT_EXIST, EXISTS_FINGERPRINT_MISMATCH, EXISTS_ADD_MIRROR, EXISTS_DISABLED, EXISTS_ENABLED, EXISTS_UPGRADABLE_TO_SIGNED, INVALID_URL, IS_SWAP } @@ -213,18 +216,35 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana private class AddRepo { private final Context context; + private final HashMap urlRepoMap = new HashMap<>(); + private final HashMap fingerprintRepoMap = new HashMap<>(); private final AlertDialog addRepoDialog; - private final TextView overwriteMessage; private final ColorStateList defaultTextColour; private final Button addButton; private AddRepoState addRepoState; + /** + * Create new instance, setup GUI, and build maps for quickly looking + * up repos based on URL or fingerprint. These need to be in maps + * since the user input is validated as they are typing. This also + * checks that the repo type matches, e.g. "repo" or "archive". + */ AddRepo(String newAddress, String newFingerprint, final String username, final String password) { context = ManageReposActivity.this; + for (Repo repo : RepoProvider.Helper.all(context)) { + urlRepoMap.put(repo.address, repo); + for (String url : repo.getMirrorList()) { + urlRepoMap.put(url, repo); + } + if (TextUtils.equals(getRepoType(newAddress), getRepoType(repo.address))) { + fingerprintRepoMap.put(repo.fingerprint, repo); + } + } + final View view = getLayoutInflater().inflate(R.layout.addrepo, null); addRepoDialog = new AlertDialog.Builder(context).setView(view).create(); final EditText uriEditText = (EditText) view.findViewById(R.id.edit_uri); @@ -297,7 +317,7 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana case EXISTS_DISABLED: case EXISTS_UPGRADABLE_TO_SIGNED: - case EXISTS_FINGERPRINT_MATCH: + case EXISTS_ADD_MIRROR: updateAndEnableExistingRepo(url, fp); finishedAddingRepo(); break; @@ -347,9 +367,31 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana validateRepoDetails(newAddress == null ? "" : newAddress, newFingerprint == null ? "" : newFingerprint); } + /** + * Gets the repo type as represented by the final segment of the path. This is + * a bit trickier with {@code content://} URLs, since they might have + * encoded "/" chars in it, for example: + * {@code content://authority/tree/313E-1F1C%3A/document/313E-1F1C%3Aguardianproject.info%2Ffdroid%2Frepo} + */ + private String getRepoType(String url) { + String last = Uri.parse(url).getLastPathSegment(); + if (last == null) { + return ""; + } else { + return new File(last).getName(); + } + } + /** * Compare the repo and the fingerprint against existing repositories, to see if this - * repo matches and display a relevant message to the user if that is the case. + * repo matches and display a relevant message to the user if that is the case. There + * are many different cases to handle: + *
    + *
  • a signed repo with a {@link Repo#address URL} and fingerprint that matches + *
  • a signed repo with a matching fingerprint and URL that matches a mirror + *
  • a signed repo with a matching fingerprint, but the URL doesn't match any known mirror + *
  • an unsigned repo and no fingerprint was supplied + *
*/ private void validateRepoDetails(@NonNull String uri, @NonNull String fingerprint) { @@ -361,7 +403,10 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana // to the user until they try to save the repo. } - final Repo repo = !TextUtils.isEmpty(uri) ? RepoProvider.Helper.findByAddress(context, uri) : null; + Repo repo = fingerprintRepoMap.get(fingerprint); + if (repo == null) { + repo = urlRepoMap.get(uri); + } if (repo == null) { repoDoesntExist(repo); @@ -373,9 +418,10 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana } else if (repo.fingerprint != null && !repo.fingerprint.equalsIgnoreCase(fingerprint)) { repoFingerprintDoesntMatch(repo); } else { - // Could be either an unsigned repo, and no fingerprint was supplied, - // or it could be a signed repo with a matching fingerprint. - if (repo.inuse) { + if (!TextUtils.equals(repo.address, uri) + && !repo.getMirrorList().contains(uri)) { + repoExistsAddMirror(repo); + } else if (repo.inuse) { repoExistsAndEnabled(repo); } else { repoExistsAndDisabled(repo); @@ -659,11 +705,34 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana } Utils.debugLog(TAG, "Enabling existing repo: " + url); - Repo repo = RepoProvider.Helper.findByAddress(context, url); + Repo repo = fingerprintRepoMap.get(fingerprint); + if (repo == null) { + repo = RepoProvider.Helper.findByAddress(context, 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); + repo.userMirrors[last] = url; + } + values.put(RepoTable.Cols.USER_MIRRORS, Utils.serializeCommaSeparatedString(repo.userMirrors)); + } + } RepoProvider.Helper.update(context, repo, values); + notifyDataSetChanged(); finishedAddingRepo(); } 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 2962ed863..f22785318 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/RepoDetailsActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/RepoDetailsActivity.java @@ -109,6 +109,7 @@ public class RepoDetailsActivity extends ActionBarActivity { RepoTable.Cols.ADDRESS, RepoTable.Cols.FINGERPRINT, RepoTable.Cols.MIRRORS, + RepoTable.Cols.USER_MIRRORS, }; repo = RepoProvider.Helper.findById(this, repoId, projection); @@ -340,6 +341,19 @@ public class RepoDetailsActivity extends ActionBarActivity { } officialMirrorsText.setText(builder.toString()); } + if (repo.userMirrors != null) { + TextView userMirrorsLabel = (TextView) repoView.findViewById(R.id.label_user_mirrors); + userMirrorsLabel.setVisibility(View.VISIBLE); + TextView userMirrorsText = (TextView) repoView.findViewById(R.id.text_user_mirrors); + userMirrorsText.setVisibility(View.VISIBLE); + StringBuilder builder = new StringBuilder(); + for (String url : repo.userMirrors) { + builder.append("◦ "); + builder.append(url); + builder.append('\n'); + } + userMirrorsText.setText(builder.toString()); + } name.setText(repo.name); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 855087c95..a38fd786a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -116,6 +116,7 @@ This often occurs with apps installed via Google Play or other sources, if they No Add new repository Add + Add mirror Links Versions More @@ -138,6 +139,7 @@ This often occurs with apps installed via Google Play or other sources, if they %1$s is already setup, confirm that you want to re-enable it. %1$s is already setup and enabled. First delete %1$s in order to add this with a conflicting key. + This is a copy of %1$s, add it as a mirror? Bad fingerprint This is not a valid URL. Ignoring malformed repo URI: %s From 70c912199560d78114e7f11acf7eee0d9c7ae2d5 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Thu, 29 Mar 2018 14:54:28 +0200 Subject: [PATCH 08/11] enforce line lengths in ManageReposActivity --- .../fdroid/views/ManageReposActivity.java | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java b/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java index e552b639a..92efbf949 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java @@ -72,8 +72,8 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Locale; -@SuppressWarnings("LineLength") -public class ManageReposActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks, RepoAdapter.EnabledListener { +public class ManageReposActivity extends AppCompatActivity + implements LoaderManager.LoaderCallbacks, RepoAdapter.EnabledListener { private static final String TAG = "ManageReposActivity"; private static final String DEFAULT_NEW_REPO_TEXT = "https://"; @@ -309,7 +309,8 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana break; case IS_SWAP: - Utils.debugLog(TAG, "Removing existing swap repo " + url + " before adding new repo."); + 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); @@ -499,7 +500,8 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana /** * Adds a new repo to the database. */ - private void prepareToCreateNewRepo(final String originalAddress, final String fingerprint, final String username, final String password) { + private void prepareToCreateNewRepo(final String originalAddress, final String fingerprint, + final String username, final String password) { addRepoDialog.findViewById(R.id.add_repo_form).setVisibility(View.GONE); addRepoDialog.getButton(AlertDialog.BUTTON_POSITIVE).setVisibility(View.GONE); @@ -518,7 +520,7 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana final String[] pathsToCheck = {"", "fdroid/repo", "repo"}; for (final String path : pathsToCheck) { - Utils.debugLog(TAG, "Checking for repo at " + originalAddress + " with suffix \"" + path + "\"."); + Utils.debugLog(TAG, "Check for repo at " + originalAddress + " with suffix '" + path + "'"); Uri.Builder builder = Uri.parse(originalAddress).buildUpon().appendEncodedPath(path); final String addressWithoutIndex = builder.build().toString(); publishProgress(addressWithoutIndex); @@ -536,7 +538,7 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana } if (isCancelled()) { - Utils.debugLog(TAG, "Not checking any more repo addresses, because process was skipped."); + Utils.debugLog(TAG, "Not checking more repo addresses, because process was skipped."); break; } } @@ -569,7 +571,8 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana if (statusCode == 401) { final View view = getLayoutInflater().inflate(R.layout.login, null); - final AlertDialog credentialsDialog = new AlertDialog.Builder(context).setView(view).create(); + final AlertDialog credentialsDialog = new AlertDialog.Builder(context) + .setView(view).create(); final EditText nameInput = (EditText) view.findViewById(R.id.edit_name); final EditText passwordInput = (EditText) view.findViewById(R.id.edit_password); @@ -597,7 +600,9 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - createNewRepo(newAddress, fingerprint, nameInput.getText().toString(), passwordInput.getText().toString()); + createNewRepo(newAddress, fingerprint, + nameInput.getText().toString(), + passwordInput.getText().toString()); } }); @@ -669,7 +674,8 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana createNewRepo(address, fingerprint, null, null); } - private void createNewRepo(String address, String fingerprint, final String username, final String password) { + private void createNewRepo(String address, String fingerprint, + final String username, final String password) { try { address = normalizeUrl(address); } catch (URISyntaxException e) { @@ -688,7 +694,7 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana RepoProvider.Helper.insert(context, values); finishedAddingRepo(); - Toast.makeText(ManageReposActivity.this, getString(R.string.repo_added, address), Toast.LENGTH_SHORT).show(); + Toast.makeText(context, getString(R.string.repo_added, address), Toast.LENGTH_SHORT).show(); } /** @@ -760,7 +766,8 @@ public class ManageReposActivity extends AppCompatActivity implements LoaderMana NewRepoConfig newRepoConfig = new NewRepoConfig(this, intent); if (newRepoConfig.isValidRepo()) { isImportingRepo = true; - showAddRepo(newRepoConfig.getRepoUriString(), newRepoConfig.getFingerprint(), newRepoConfig.getUsername(), newRepoConfig.getPassword()); + showAddRepo(newRepoConfig.getRepoUriString(), newRepoConfig.getFingerprint(), + newRepoConfig.getUsername(), newRepoConfig.getPassword()); checkIfNewRepoOnSameWifi(newRepoConfig); } else if (newRepoConfig.getErrorMessage() != null) { Toast.makeText(this, newRepoConfig.getErrorMessage(), Toast.LENGTH_LONG).show(); From a88b9c924e4a5d18069acd7b1793e8b67d946f73 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Thu, 29 Mar 2018 16:12:32 +0200 Subject: [PATCH 09/11] when validating added repo URLs, check whether they're already included --- .../fdroid/views/ManageReposActivity.java | 41 +++++++++++++++---- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java b/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java index 92efbf949..0ce35a301 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java @@ -19,6 +19,7 @@ package org.fdroid.fdroid.views; +import android.annotation.SuppressLint; import android.content.ContentValues; import android.content.Context; import android.content.DialogInterface; @@ -500,23 +501,37 @@ public class ManageReposActivity extends AppCompatActivity /** * Adds a new repo to the database. */ + @SuppressLint("StaticFieldLeak") private void prepareToCreateNewRepo(final String originalAddress, final String fingerprint, final String username, final String password) { - addRepoDialog.findViewById(R.id.add_repo_form).setVisibility(View.GONE); - addRepoDialog.getButton(AlertDialog.BUTTON_POSITIVE).setVisibility(View.GONE); + final View addRepoForm = addRepoDialog.findViewById(R.id.add_repo_form); + addRepoForm.setVisibility(View.GONE); + final View positiveButton = addRepoDialog.getButton(AlertDialog.BUTTON_POSITIVE); + positiveButton.setVisibility(View.GONE); final TextView textSearching = (TextView) addRepoDialog.findViewById(R.id.text_searching_for_repo); textSearching.setText(getString(R.string.repo_searching_address, originalAddress)); + final Button skip = addRepoDialog.getButton(AlertDialog.BUTTON_NEGATIVE); + skip.setText(R.string.skip); + final AsyncTask checker = new AsyncTask() { private int statusCode = -1; + private final static int REFRESH_DIALOG = Integer.MAX_VALUE; + private final static int HTTP_UNAUTHORIZED = 401; + private final static int HTTP_OK = 200; @Override protected String doInBackground(String... params) { - final String originalAddress = params[0]; + + if (fingerprintRepoMap.containsKey(fingerprint)) { + statusCode = REFRESH_DIALOG; + return originalAddress; + } + final String[] pathsToCheck = {"", "fdroid/repo", "repo"}; for (final String path : pathsToCheck) { @@ -525,6 +540,11 @@ public class ManageReposActivity extends AppCompatActivity final String addressWithoutIndex = builder.build().toString(); publishProgress(addressWithoutIndex); + if (urlRepoMap.containsKey(addressWithoutIndex)) { + statusCode = REFRESH_DIALOG; + return addressWithoutIndex; + } + final Uri uri = builder.appendPath("index.jar").build(); try { @@ -547,14 +567,13 @@ public class ManageReposActivity extends AppCompatActivity } private boolean checkForRepository(Uri indexUri) throws IOException { - final URL url = new URL(indexUri.toString()); final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("HEAD"); statusCode = connection.getResponseCode(); - return statusCode == 401 || statusCode == 200; + return statusCode == HTTP_UNAUTHORIZED || statusCode == HTTP_OK; } @Override @@ -568,7 +587,7 @@ public class ManageReposActivity extends AppCompatActivity if (addRepoDialog.isShowing()) { - if (statusCode == 401) { + if (statusCode == HTTP_UNAUTHORIZED) { final View view = getLayoutInflater().inflate(R.layout.login, null); final AlertDialog credentialsDialog = new AlertDialog.Builder(context) @@ -608,6 +627,13 @@ public class ManageReposActivity extends AppCompatActivity credentialsDialog.show(); + } else if (statusCode == REFRESH_DIALOG) { + addRepoForm.setVisibility(View.VISIBLE); + positiveButton.setVisibility(View.VISIBLE); + textSearching.setText(""); + skip.setText(R.string.cancel); + skip.setOnClickListener(null); + validateRepoDetails(newAddress, fingerprint); } else { // create repo without username/password @@ -617,8 +643,6 @@ public class ManageReposActivity extends AppCompatActivity } }; - Button skip = addRepoDialog.getButton(AlertDialog.BUTTON_NEGATIVE); - skip.setText(R.string.skip); skip.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -758,7 +782,6 @@ public class ManageReposActivity extends AppCompatActivity finish(); } } - } private void addRepoFromIntent(Intent intent) { From 195aaae7e52dc1c47741965904ed17bdc816a71c Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Thu, 29 Mar 2018 17:03:00 +0200 Subject: [PATCH 10/11] switch Downloader total download size to long to support >16MB This was int because it was written arond UrlConnection.getContentLength() which returns an int. But that doesn't make sense since this will definitely handle files large than 16MB. !647 #1192 --- .../main/java/org/fdroid/fdroid/ProgressListener.java | 2 +- app/src/main/java/org/fdroid/fdroid/RepoUpdater.java | 4 ++-- .../main/java/org/fdroid/fdroid/UpdateService.java | 10 ++++++---- .../org/fdroid/fdroid/net/BluetoothDownloader.java | 2 +- .../main/java/org/fdroid/fdroid/net/Downloader.java | 6 +++--- .../java/org/fdroid/fdroid/net/DownloaderService.java | 2 +- .../java/org/fdroid/fdroid/net/HttpDownloader.java | 11 +++++++++-- .../org/fdroid/fdroid/net/bluetooth/FileDetails.java | 4 ++-- 8 files changed, 25 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/ProgressListener.java b/app/src/main/java/org/fdroid/fdroid/ProgressListener.java index 1e71373ad..beb370fd2 100644 --- a/app/src/main/java/org/fdroid/fdroid/ProgressListener.java +++ b/app/src/main/java/org/fdroid/fdroid/ProgressListener.java @@ -19,6 +19,6 @@ import java.net.URL; */ public interface ProgressListener { - void onProgress(URL sourceUrl, int bytesRead, int totalBytes); + void onProgress(URL sourceUrl, long bytesRead, long totalBytes); } diff --git a/app/src/main/java/org/fdroid/fdroid/RepoUpdater.java b/app/src/main/java/org/fdroid/fdroid/RepoUpdater.java index 9c3463b7f..afb3659ab 100644 --- a/app/src/main/java/org/fdroid/fdroid/RepoUpdater.java +++ b/app/src/main/java/org/fdroid/fdroid/RepoUpdater.java @@ -251,14 +251,14 @@ public class RepoUpdater { protected final ProgressListener downloadListener = new ProgressListener() { @Override - public void onProgress(URL sourceUrl, int bytesRead, int totalBytes) { + public void onProgress(URL sourceUrl, long bytesRead, long totalBytes) { UpdateService.reportDownloadProgress(context, RepoUpdater.this, bytesRead, totalBytes); } }; protected final ProgressListener processIndexListener = new ProgressListener() { @Override - public void onProgress(URL sourceUrl, int bytesRead, int totalBytes) { + public void onProgress(URL sourceUrl, long bytesRead, long totalBytes) { UpdateService.reportProcessIndexProgress(context, RepoUpdater.this, bytesRead, totalBytes); } }; diff --git a/app/src/main/java/org/fdroid/fdroid/UpdateService.java b/app/src/main/java/org/fdroid/fdroid/UpdateService.java index 1b9a5a62b..5b233046f 100644 --- a/app/src/main/java/org/fdroid/fdroid/UpdateService.java +++ b/app/src/main/java/org/fdroid/fdroid/UpdateService.java @@ -516,12 +516,13 @@ public class UpdateService extends IntentService { } } - public static void reportDownloadProgress(Context context, RepoUpdater updater, int bytesRead, int totalBytes) { + public static void reportDownloadProgress(Context context, RepoUpdater updater, + long bytesRead, long totalBytes) { Utils.debugLog(TAG, "Downloading " + updater.indexUrl + "(" + bytesRead + "/" + totalBytes + ")"); String downloadedSizeFriendly = Utils.getFriendlySize(bytesRead); int percent = -1; if (totalBytes > 0) { - percent = (int) ((double) bytesRead / totalBytes * 100); + percent = (int) (bytesRead / (totalBytes * 100L)); } String message; if (totalBytes == -1) { @@ -534,13 +535,14 @@ public class UpdateService extends IntentService { sendStatus(context, STATUS_INFO, message, percent); } - public static void reportProcessIndexProgress(Context context, RepoUpdater updater, int bytesRead, int totalBytes) { + public static void reportProcessIndexProgress(Context context, RepoUpdater updater, + long bytesRead, long totalBytes) { Utils.debugLog(TAG, "Processing " + updater.indexUrl + "(" + bytesRead + "/" + totalBytes + ")"); String downloadedSize = Utils.getFriendlySize(bytesRead); String totalSize = Utils.getFriendlySize(totalBytes); int percent = -1; if (totalBytes > 0) { - percent = (int) ((double) bytesRead / totalBytes * 100); + percent = (int) (bytesRead / (totalBytes * 100L)); } String message = context.getString(R.string.status_processing_xml_percent, updater.indexUrl, downloadedSize, totalSize, percent); sendStatus(context, STATUS_INFO, message, percent); diff --git a/app/src/main/java/org/fdroid/fdroid/net/BluetoothDownloader.java b/app/src/main/java/org/fdroid/fdroid/net/BluetoothDownloader.java index 8ab0c4704..fd9a06379 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/BluetoothDownloader.java +++ b/app/src/main/java/org/fdroid/fdroid/net/BluetoothDownloader.java @@ -73,7 +73,7 @@ public class BluetoothDownloader extends Downloader { } @Override - public int totalDownloadSize() { + public long totalDownloadSize() { FileDetails details = getFileDetails(); return details != null ? details.getFileSize() : -1; } diff --git a/app/src/main/java/org/fdroid/fdroid/net/Downloader.java b/app/src/main/java/org/fdroid/fdroid/net/Downloader.java index 885dc3d79..d391ef2b6 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/Downloader.java +++ b/app/src/main/java/org/fdroid/fdroid/net/Downloader.java @@ -32,8 +32,8 @@ public abstract class Downloader { public static final String EXTRA_MIRROR_URL = "org.fdroid.fdroid.net.Downloader.extra.ERROR_MIRROR_URL"; private volatile boolean cancelled = false; - private volatile int bytesRead; - private volatile int totalBytes; + private volatile long bytesRead; + private volatile long totalBytes; public final File outputFile; @@ -92,7 +92,7 @@ public abstract class Downloader { public abstract boolean hasChanged(); - protected abstract int totalDownloadSize(); + protected abstract long totalDownloadSize(); public abstract void download() throws ConnectException, IOException, InterruptedException; diff --git a/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java b/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java index ca63ec17c..52d474e45 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java +++ b/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java @@ -199,7 +199,7 @@ public class DownloaderService extends Service { downloader = DownloaderFactory.create(this, uri, localFile); downloader.setListener(new ProgressListener() { @Override - public void onProgress(URL sourceUrl, int bytesRead, int totalBytes) { + public void onProgress(URL sourceUrl, long bytesRead, long totalBytes) { Intent intent = new Intent(Downloader.ACTION_PROGRESS); intent.setData(uri); intent.putExtra(Downloader.EXTRA_BYTES_READ, bytesRead); diff --git a/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java b/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java index c44f94487..05b460473 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java +++ b/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java @@ -1,5 +1,7 @@ package org.fdroid.fdroid.net; +import android.annotation.TargetApi; +import android.os.Build; import android.text.TextUtils; import com.nostra13.universalimageloader.core.download.BaseImageDownloader; import info.guardianproject.netcipher.NetCipher; @@ -169,8 +171,13 @@ public class HttpDownloader extends Downloader { // because as the repo grows, the tradeoff will // become more worth it. @Override - public int totalDownloadSize() { - return connection.getContentLength(); + @TargetApi(24) + public long totalDownloadSize() { + if (Build.VERSION.SDK_INT < 24) { + return connection.getContentLength(); + } else { + return connection.getContentLengthLong(); + } } @Override diff --git a/app/src/main/java/org/fdroid/fdroid/net/bluetooth/FileDetails.java b/app/src/main/java/org/fdroid/fdroid/net/bluetooth/FileDetails.java index f7148a91f..96c57f63c 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/bluetooth/FileDetails.java +++ b/app/src/main/java/org/fdroid/fdroid/net/bluetooth/FileDetails.java @@ -3,13 +3,13 @@ package org.fdroid.fdroid.net.bluetooth; public class FileDetails { private String cacheTag; - private int fileSize; + private long fileSize; public String getCacheTag() { return cacheTag; } - public int getFileSize() { + public long getFileSize() { return fileSize; } From df08e84e7829652d7999eee5451080a012b00a1e Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Thu, 29 Mar 2018 22:26:41 +0200 Subject: [PATCH 11/11] switch all Downloader subclasses to use Uri instead of URL java.net.URL barfs on custom URL schemes, and making it handle them is really hard. Basically, there needs to be a Handler stub class, then URL.setURLStreamHandlerFactory() must run when F-Droid starts, since it has to be set before any URL instance is used. This all leaves some weird logic that gives the false impression that URLConnection will handle these custom schemes. Switching to Uri/urlString throughout the code matches the other classes that use urlString as the unique ID, and this doesn't add more lines of code. --- .../java/org/fdroid/fdroid/FDroidApp.java | 14 ------------- .../org/fdroid/fdroid/IndexV1Updater.java | 5 +---- .../fdroid/ProgressBufferedInputStream.java | 9 ++++---- .../org/fdroid/fdroid/ProgressListener.java | 2 +- .../java/org/fdroid/fdroid/RepoUpdater.java | 7 +++---- .../fdroid/net/BluetoothDownloader.java | 12 +++++------ .../org/fdroid/fdroid/net/Downloader.java | 10 ++++----- .../fdroid/fdroid/net/DownloaderFactory.java | 21 +++++++------------ .../fdroid/fdroid/net/DownloaderService.java | 6 ++---- .../org/fdroid/fdroid/net/HttpDownloader.java | 20 ++++++++++-------- .../net/www/protocol/bluetooth/Handler.java | 18 ---------------- 11 files changed, 40 insertions(+), 84 deletions(-) delete mode 100644 app/src/main/java/sun/net/www/protocol/bluetooth/Handler.java diff --git a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java index 58b16de3f..0d3a05068 100644 --- a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java +++ b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java @@ -60,12 +60,8 @@ import org.fdroid.fdroid.installer.InstallHistoryService; import org.fdroid.fdroid.net.ImageLoaderForUIL; import org.fdroid.fdroid.net.WifiStateChangeService; import org.fdroid.fdroid.views.hiding.HidingManager; -import sun.net.www.protocol.bluetooth.Handler; import java.io.IOException; -import java.net.URL; -import java.net.URLStreamHandler; -import java.net.URLStreamHandlerFactory; import java.security.Security; import java.util.List; @@ -370,16 +366,6 @@ public class FDroidApp extends Application { } }); - // This is added so that the bluetooth:// scheme we use for URLs the BluetoothDownloader - // understands is not treated as invalid by the java.net.URL class. The actual Handler does - // nothing, but its presence is enough. - URL.setURLStreamHandlerFactory(new URLStreamHandlerFactory() { - @Override - public URLStreamHandler createURLStreamHandler(String protocol) { - return TextUtils.equals(protocol, "bluetooth") ? new Handler() : null; - } - }); - final Context context = this; Preferences.get().registerUnstableUpdatesChangeListener(new Preferences.ChangeListener() { @Override diff --git a/app/src/main/java/org/fdroid/fdroid/IndexV1Updater.java b/app/src/main/java/org/fdroid/fdroid/IndexV1Updater.java index b53a6a17e..b961f3abb 100644 --- a/app/src/main/java/org/fdroid/fdroid/IndexV1Updater.java +++ b/app/src/main/java/org/fdroid/fdroid/IndexV1Updater.java @@ -6,7 +6,6 @@ import android.net.Uri; import android.support.annotation.NonNull; import android.text.TextUtils; import android.util.Log; - import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.core.JsonFactory; @@ -15,7 +14,6 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.InjectableValues; import com.fasterxml.jackson.databind.ObjectMapper; - import org.apache.commons.io.FileUtils; import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.App; @@ -31,7 +29,6 @@ import java.io.IOException; import java.io.InputStream; import java.net.ConnectException; import java.net.SocketTimeoutException; -import java.net.URL; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Date; @@ -157,7 +154,7 @@ public class IndexV1Updater extends RepoUpdater { JarFile jarFile = new JarFile(outputFile, true); JarEntry indexEntry = (JarEntry) jarFile.getEntry(DATA_FILE_NAME); InputStream indexInputStream = new ProgressBufferedInputStream(jarFile.getInputStream(indexEntry), - processIndexListener, new URL(repo.address), (int) indexEntry.getSize()); + processIndexListener, repo.address, (int) indexEntry.getSize()); processIndexV1(indexInputStream, indexEntry, cacheTag); } diff --git a/app/src/main/java/org/fdroid/fdroid/ProgressBufferedInputStream.java b/app/src/main/java/org/fdroid/fdroid/ProgressBufferedInputStream.java index d8ca7ca9f..e849fe3cb 100644 --- a/app/src/main/java/org/fdroid/fdroid/ProgressBufferedInputStream.java +++ b/app/src/main/java/org/fdroid/fdroid/ProgressBufferedInputStream.java @@ -3,12 +3,11 @@ package org.fdroid.fdroid; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; -import java.net.URL; class ProgressBufferedInputStream extends BufferedInputStream { private final ProgressListener progressListener; - private final URL sourceUrl; + private final String urlString; private final int totalBytes; private int currentBytes; @@ -17,10 +16,10 @@ class ProgressBufferedInputStream extends BufferedInputStream { * Reports progress to the specified {@link ProgressListener}, with the * progress based on the {@code totalBytes}. */ - ProgressBufferedInputStream(InputStream in, ProgressListener progressListener, URL sourceUrl, int totalBytes) { + ProgressBufferedInputStream(InputStream in, ProgressListener progressListener, String urlString, int totalBytes) { super(in); this.progressListener = progressListener; - this.sourceUrl = sourceUrl; + this.urlString = urlString; this.totalBytes = totalBytes; } @@ -32,7 +31,7 @@ class ProgressBufferedInputStream extends BufferedInputStream { * the digits changing because it looks pretty, < 9000 since the reads won't * line up exactly */ if (currentBytes % 333333 < 9000) { - progressListener.onProgress(sourceUrl, currentBytes, totalBytes); + progressListener.onProgress(urlString, currentBytes, totalBytes); } } return super.read(buffer, byteOffset, byteCount); diff --git a/app/src/main/java/org/fdroid/fdroid/ProgressListener.java b/app/src/main/java/org/fdroid/fdroid/ProgressListener.java index beb370fd2..dc9fbcbdb 100644 --- a/app/src/main/java/org/fdroid/fdroid/ProgressListener.java +++ b/app/src/main/java/org/fdroid/fdroid/ProgressListener.java @@ -19,6 +19,6 @@ import java.net.URL; */ public interface ProgressListener { - void onProgress(URL sourceUrl, long bytesRead, long totalBytes); + void onProgress(String urlString, long bytesRead, long totalBytes); } diff --git a/app/src/main/java/org/fdroid/fdroid/RepoUpdater.java b/app/src/main/java/org/fdroid/fdroid/RepoUpdater.java index afb3659ab..6e8654d89 100644 --- a/app/src/main/java/org/fdroid/fdroid/RepoUpdater.java +++ b/app/src/main/java/org/fdroid/fdroid/RepoUpdater.java @@ -53,7 +53,6 @@ import javax.xml.parsers.SAXParserFactory; import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.net.URL; import java.security.CodeSigner; import java.security.cert.Certificate; import java.security.cert.X509Certificate; @@ -213,7 +212,7 @@ public class RepoUpdater { JarFile jarFile = new JarFile(downloadedFile, true); JarEntry indexEntry = (JarEntry) jarFile.getEntry("index.xml"); indexInputStream = new ProgressBufferedInputStream(jarFile.getInputStream(indexEntry), - processIndexListener, new URL(repo.address), (int) indexEntry.getSize()); + processIndexListener, repo.address, (int) indexEntry.getSize()); // Process the index... SAXParserFactory factory = SAXParserFactory.newInstance(); @@ -251,14 +250,14 @@ public class RepoUpdater { protected final ProgressListener downloadListener = new ProgressListener() { @Override - public void onProgress(URL sourceUrl, long bytesRead, long totalBytes) { + public void onProgress(String urlString, long bytesRead, long totalBytes) { UpdateService.reportDownloadProgress(context, RepoUpdater.this, bytesRead, totalBytes); } }; protected final ProgressListener processIndexListener = new ProgressListener() { @Override - public void onProgress(URL sourceUrl, long bytesRead, long totalBytes) { + public void onProgress(String urlString, long bytesRead, long totalBytes) { UpdateService.reportProcessIndexProgress(context, RepoUpdater.this, bytesRead, totalBytes); } }; diff --git a/app/src/main/java/org/fdroid/fdroid/net/BluetoothDownloader.java b/app/src/main/java/org/fdroid/fdroid/net/BluetoothDownloader.java index fd9a06379..85d4f4d84 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/BluetoothDownloader.java +++ b/app/src/main/java/org/fdroid/fdroid/net/BluetoothDownloader.java @@ -1,8 +1,8 @@ package org.fdroid.fdroid.net; +import android.net.Uri; import android.support.annotation.Nullable; import android.util.Log; - import org.apache.commons.io.input.BoundedInputStream; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.net.bluetooth.BluetoothClient; @@ -14,7 +14,6 @@ import org.fdroid.fdroid.net.bluetooth.httpish.Response; import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.net.URL; public class BluetoothDownloader extends Downloader { @@ -24,10 +23,11 @@ public class BluetoothDownloader extends Downloader { private FileDetails fileDetails; private final String sourcePath; - public BluetoothDownloader(String macAddress, URL sourceUrl, File destFile) throws IOException { - super(sourceUrl, destFile); + public BluetoothDownloader(Uri uri, File destFile) throws IOException { + super(uri, destFile); + String macAddress = uri.getHost().replace("-", ":"); this.connection = new BluetoothClient(macAddress).openConnection(); - this.sourcePath = sourceUrl.getPath(); + this.sourcePath = uri.getPath(); } @Override @@ -58,7 +58,7 @@ public class BluetoothDownloader extends Downloader { if (fileDetails == null) { Utils.debugLog(TAG, "Going to Bluetooth \"server\" to get file details."); try { - fileDetails = Request.createHEAD(sourceUrl.getPath(), connection).send().toFileDetails(); + fileDetails = Request.createHEAD(sourcePath, connection).send().toFileDetails(); } catch (IOException e) { Log.e(TAG, "Error getting file details from Bluetooth \"server\"", e); } diff --git a/app/src/main/java/org/fdroid/fdroid/net/Downloader.java b/app/src/main/java/org/fdroid/fdroid/net/Downloader.java index d391ef2b6..3dcb80479 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/Downloader.java +++ b/app/src/main/java/org/fdroid/fdroid/net/Downloader.java @@ -1,5 +1,6 @@ package org.fdroid.fdroid.net; +import android.net.Uri; import org.fdroid.fdroid.ProgressListener; import org.fdroid.fdroid.Utils; @@ -9,7 +10,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ConnectException; -import java.net.URL; import java.util.Timer; import java.util.TimerTask; @@ -37,7 +37,7 @@ public abstract class Downloader { public final File outputFile; - final URL sourceUrl; + final String urlString; String cacheTag; boolean notFound; @@ -52,8 +52,8 @@ public abstract class Downloader { protected abstract void close(); - Downloader(URL url, File destFile) { - this.sourceUrl = url; + Downloader(Uri uri, File destFile) { + this.urlString = uri.toString(); outputFile = destFile; } @@ -201,7 +201,7 @@ public abstract class Downloader { @Override public void run() { if (downloaderProgressListener != null) { - downloaderProgressListener.onProgress(sourceUrl, bytesRead, totalBytes); + downloaderProgressListener.onProgress(urlString, bytesRead, totalBytes); } } }; diff --git a/app/src/main/java/org/fdroid/fdroid/net/DownloaderFactory.java b/app/src/main/java/org/fdroid/fdroid/net/DownloaderFactory.java index 8e87b4347..3d124eed9 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/DownloaderFactory.java +++ b/app/src/main/java/org/fdroid/fdroid/net/DownloaderFactory.java @@ -2,19 +2,15 @@ package org.fdroid.fdroid.net; import android.content.Context; import android.net.Uri; -import android.support.v4.content.LocalBroadcastManager; import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.data.Schema; import java.io.File; import java.io.IOException; -import java.net.URL; public class DownloaderFactory { - private static LocalBroadcastManager localBroadcastManager; - /** * Downloads to a temporary file, which *you must delete yourself when * you are done. It is stored in {@link Context#getCacheDir()} and starts @@ -34,22 +30,19 @@ public class DownloaderFactory { public static Downloader create(Context context, String urlString, File destFile) throws IOException { - URL url = new URL(urlString); Downloader downloader; - if (localBroadcastManager == null) { - localBroadcastManager = LocalBroadcastManager.getInstance(context); - } - if ("bluetooth".equalsIgnoreCase(url.getProtocol())) { - String macAddress = url.getHost().replace("-", ":"); - downloader = new BluetoothDownloader(macAddress, url, destFile); + Uri uri = Uri.parse(urlString); + String scheme = uri.getScheme(); + if ("bluetooth".equals(scheme)) { + downloader = new BluetoothDownloader(uri, destFile); } else { final String[] projection = {Schema.RepoTable.Cols.USERNAME, Schema.RepoTable.Cols.PASSWORD}; - Repo repo = RepoProvider.Helper.findByUrl(context, Uri.parse(url.toString()), projection); + Repo repo = RepoProvider.Helper.findByUrl(context, uri, projection); if (repo == null) { - downloader = new HttpDownloader(url, destFile); + downloader = new HttpDownloader(uri, destFile); } else { - downloader = new HttpDownloader(url, destFile, repo.username, repo.password); + downloader = new HttpDownloader(uri, destFile, repo.username, repo.password); } } return downloader; diff --git a/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java b/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java index 52d474e45..cf564bab9 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java +++ b/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java @@ -31,7 +31,6 @@ import android.os.PatternMatcher; import android.os.Process; import android.support.v4.content.LocalBroadcastManager; import android.text.TextUtils; - import org.fdroid.fdroid.ProgressListener; import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; @@ -42,7 +41,6 @@ import java.io.File; import java.io.IOException; import java.net.ConnectException; import java.net.SocketTimeoutException; -import java.net.URL; /** * DownloaderService is a service that handles asynchronous download requests @@ -199,7 +197,7 @@ public class DownloaderService extends Service { downloader = DownloaderFactory.create(this, uri, localFile); downloader.setListener(new ProgressListener() { @Override - public void onProgress(URL sourceUrl, long bytesRead, long totalBytes) { + public void onProgress(String urlString, long bytesRead, long totalBytes) { Intent intent = new Intent(Downloader.ACTION_PROGRESS); intent.setData(uri); intent.putExtra(Downloader.EXTRA_BYTES_READ, bytesRead); @@ -321,7 +319,7 @@ public class DownloaderService extends Service { * Check if a URL is actively being downloaded. */ private static boolean isActive(String urlString) { - return downloader != null && TextUtils.equals(urlString, downloader.sourceUrl.toString()); + return downloader != null && TextUtils.equals(urlString, downloader.urlString); } public static void setTimeout(int ms) { diff --git a/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java b/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java index 05b460473..8dba014de 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java +++ b/app/src/main/java/org/fdroid/fdroid/net/HttpDownloader.java @@ -1,6 +1,7 @@ package org.fdroid.fdroid.net; import android.annotation.TargetApi; +import android.net.Uri; import android.os.Build; import android.text.TextUtils; import com.nostra13.universalimageloader.core.download.BaseImageDownloader; @@ -29,29 +30,30 @@ public class HttpDownloader extends Downloader { private final String username; private final String password; + private URL sourceUrl; private HttpURLConnection connection; private boolean newFileAvailableOnServer; - HttpDownloader(URL url, File destFile) + HttpDownloader(Uri uri, File destFile) throws FileNotFoundException, MalformedURLException { - this(url, destFile, null, null); + this(uri, destFile, null, null); } /** * Create a downloader that can authenticate via HTTP Basic Auth using the supplied * {@code username} and {@code password}. * - * @param url The file to download + * @param uri The file to download * @param destFile Where the download is saved * @param username Username for HTTP Basic Auth, use {@code null} to ignore * @param password Password for HTTP Basic Auth, use {@code null} to ignore * @throws FileNotFoundException * @throws MalformedURLException */ - HttpDownloader(URL url, File destFile, String username, String password) + HttpDownloader(Uri uri, File destFile, String username, String password) throws FileNotFoundException, MalformedURLException { - super(url, destFile); - + super(uri, destFile); + this.sourceUrl = new URL(urlString); this.username = username; this.password = password; } @@ -95,7 +97,7 @@ public class HttpDownloader extends Downloader { case 200: contentLength = tmpConn.getContentLength(); if (!TextUtils.isEmpty(etag) && etag.equals(cacheTag)) { - Utils.debugLog(TAG, sourceUrl + " is cached, not downloading"); + Utils.debugLog(TAG, urlString + " is cached, not downloading"); return; } newFileAvailableOnServer = true; @@ -104,7 +106,7 @@ public class HttpDownloader extends Downloader { notFound = true; return; default: - Utils.debugLog(TAG, "HEAD check of " + sourceUrl + " returned " + statusCode + ": " + Utils.debugLog(TAG, "HEAD check of " + urlString + " returned " + statusCode + ": " + tmpConn.getResponseMessage()); } @@ -118,7 +120,7 @@ public class HttpDownloader extends Downloader { resumable = true; } setupConnection(resumable); - Utils.debugLog(TAG, "downloading " + sourceUrl + " (is resumable: " + resumable + ")"); + Utils.debugLog(TAG, "downloading " + urlString + " (is resumable: " + resumable + ")"); downloadFromStream(8192, resumable); cacheTag = connection.getHeaderField(HEADER_FIELD_ETAG); } diff --git a/app/src/main/java/sun/net/www/protocol/bluetooth/Handler.java b/app/src/main/java/sun/net/www/protocol/bluetooth/Handler.java deleted file mode 100644 index 34234ee89..000000000 --- a/app/src/main/java/sun/net/www/protocol/bluetooth/Handler.java +++ /dev/null @@ -1,18 +0,0 @@ -package sun.net.www.protocol.bluetooth; - -import java.io.IOException; -import java.net.URL; -import java.net.URLConnection; -import java.net.URLStreamHandler; - -/** - * This class is added so that the bluetooth:// scheme we use for the {@link - * org.fdroid.fdroid.net.BluetoothDownloader} is not treated as invalid by - * the {@link URL} class. - */ -public class Handler extends URLStreamHandler { - @Override - protected URLConnection openConnection(URL u) throws IOException { - throw new UnsupportedOperationException("openConnection() not supported on bluetooth:// URLs"); - } -}